├── addons.xml.md5 ├── plugin.video.tumblrv ├── resources │ ├── __init__.py │ ├── lib │ │ ├── pytumblr │ │ │ ├── oauth2 │ │ │ │ ├── clients │ │ │ │ │ ├── __init__.py │ │ │ │ │ ├── imap.py │ │ │ │ │ └── smtp.py │ │ │ │ ├── .DS_Store │ │ │ │ ├── _version.py │ │ │ │ └── __init__.py │ │ │ ├── .DS_Store │ │ │ ├── helpers.py │ │ │ ├── request.py │ │ │ └── __init__.py │ │ ├── .DS_Store │ │ ├── tumblrsearch.py │ │ ├── __init__.py │ │ └── tumblr.py │ ├── .DS_Store │ ├── images │ │ ├── next.png │ │ ├── folder.png │ │ ├── liked.png │ │ ├── search.png │ │ ├── tumblr.png │ │ ├── following.png │ │ └── blackfolder.png │ ├── language │ │ ├── .DS_Store │ │ └── English │ │ │ └── strings.po │ └── settings.xml ├── icon.png ├── .DS_Store ├── plugin.video.tumblrv-0.9.7.zip ├── addon.xml └── addon.py ├── zips ├── .DS_Store ├── plugin.video.tumblrv │ ├── icon.png │ ├── plugin.video.tumblrv-0.9.3.zip │ ├── plugin.video.tumblrv-0.9.4.zip │ ├── plugin.video.tumblrv-0.9.5.zip │ ├── plugin.video.tumblrv-0.9.6.zip │ ├── addon.xml │ └── addon.py └── script.module.oauth2-1.5.211.zip ├── .idea └── vcs.xml ├── README.md ├── addons.xml └── addons_xml_generator2.py /addons.xml.md5: -------------------------------------------------------------------------------- 1 | 9f2a00711224908a4f6bfe5bfd0bc318 -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/clients/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /zips/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/.DS_Store -------------------------------------------------------------------------------- /plugin.video.tumblrv/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/icon.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/.DS_Store -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/plugin.video.tumblrv/icon.png -------------------------------------------------------------------------------- /zips/script.module.oauth2-1.5.211.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/script.module.oauth2-1.5.211.zip -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/.DS_Store -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/lib/.DS_Store -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/next.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/folder.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/liked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/liked.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/search.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/tumblr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/tumblr.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/language/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/language/.DS_Store -------------------------------------------------------------------------------- /plugin.video.tumblrv/plugin.video.tumblrv-0.9.7.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/plugin.video.tumblrv-0.9.7.zip -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/following.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/following.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/images/blackfolder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/images/blackfolder.png -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/lib/pytumblr/.DS_Store -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.3.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.3.zip -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.4.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.4.zip -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.5.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.5.zip -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.6.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/zips/plugin.video.tumblrv/plugin.video.tumblrv-0.9.6.zip -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moedje/TumblrVideos/HEAD/plugin.video.tumblrv/resources/lib/pytumblr/oauth2/.DS_Store -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/_version.py: -------------------------------------------------------------------------------- 1 | # This is the version of this source code. 2 | 3 | manual_verstr = "1.5" 4 | 5 | 6 | 7 | auto_build_num = "211" 8 | 9 | 10 | 11 | verstr = manual_verstr + "." + auto_build_num 12 | try: 13 | from pyutil.version_class import Version as pyutil_Version 14 | __version__ = pyutil_Version(verstr) 15 | except (ImportError, ValueError): 16 | # Maybe there is no pyutil installed. 17 | from distutils.version import LooseVersion as distutils_Version 18 | __version__ = distutils_Version(verstr) 19 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/language/English/strings.po: -------------------------------------------------------------------------------- 1 | # Kodi Media Center language file 2 | # Addon Name: tumblrV 3 | # Addon id: plugin.video.tumblrv 4 | # Addon Provider: moedje 5 | msgid "" 6 | msgstr "" 7 | "Project-Id-Version: Kodi Addons\n" 8 | "Report-Msgid-Bugs-To: \n" 9 | "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n" 10 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 11 | "Last-Translator: \n" 12 | "Language-Team: \n" 13 | "MIME-Version: 1.0\n" 14 | "Content-Type: text/plain; charset=UTF-8\n" 15 | "Content-Transfer-Encoding: 8bit\n" 16 | "Language: en\n" 17 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 18 | 19 | # strings 30000 thru 30999 reserved for plugins and plugin settings 20 | # strings 31000 thru 31999 reserved for skins 21 | # strings 32000 thru 32999 reserved for scripts 22 | # strings 33000 thru 33999 reserved for common strings used in add-ons 23 | 24 | msgctxt "#33000" 25 | msgid "Hello Kodi" 26 | msgstr "" 27 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/clients/imap.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import oauth2 26 | import imaplib 27 | 28 | 29 | class IMAP4_SSL(imaplib.IMAP4_SSL): 30 | """IMAP wrapper for imaplib.IMAP4_SSL that implements XOAUTH.""" 31 | 32 | def authenticate(self, url, consumer, token): 33 | if consumer is not None and not isinstance(consumer, oauth2.Consumer): 34 | raise ValueError("Invalid consumer.") 35 | 36 | if token is not None and not isinstance(token, oauth2.Token): 37 | raise ValueError("Invalid token.") 38 | 39 | imaplib.IMAP4_SSL.authenticate(self, 'XOAUTH', 40 | lambda x: oauth2.build_xoauth_string(url, consumer, token)) 41 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/clients/smtp.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import oauth2 26 | import smtplib 27 | import base64 28 | 29 | 30 | class SMTP(smtplib.SMTP): 31 | """SMTP wrapper for smtplib.SMTP that implements XOAUTH.""" 32 | 33 | def authenticate(self, url, consumer, token): 34 | if consumer is not None and not isinstance(consumer, oauth2.Consumer): 35 | raise ValueError("Invalid consumer.") 36 | 37 | if token is not None and not isinstance(token, oauth2.Token): 38 | raise ValueError("Invalid token.") 39 | 40 | self.docmd('AUTH', 'XOAUTH %s' % \ 41 | base64.b64encode(oauth2.build_xoauth_string(url, consumer, token))) 42 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/tumblrsearch.py: -------------------------------------------------------------------------------- 1 | import pytumblr 2 | import datetime 3 | import os 4 | 5 | def getAllPosts (client, blog): 6 | offset = 0 7 | allposts = [] 8 | more = True 9 | while more: 10 | post_response = client.posts(blog, limit=20, offset=offset, filter="text").get('posts', []) 11 | if len(post_response) < 1: 12 | more = False 13 | else: 14 | allposts.extend(post_response) 15 | offset += 20 16 | return allposts 17 | 18 | def any_keyword_in(keywords, string): 19 | for keyword in keywords: 20 | if keyword in string: 21 | return True 22 | return False 23 | 24 | def search(keywords, client, tumblr_url, write_to_datefile=False): 25 | if not isinstance(keywords, list): 26 | raise Exception("search keywords must be a list") 27 | items = [] 28 | posts = getAllPosts(client, tumblr_url) 29 | for post in posts: 30 | body = post.get('body', " ").encode("utf-8") + ' ' + post.get('caption', " ").encode("utf-8") + ' ' + post.get('source_title', " ").encode("utf-8") + ' ' + post.get('summary', " ").encode("utf-8")+ ' ' + str(post.get('tags', [])[:]) 31 | # think this is right 32 | timestamp = post.get('timestamp') 33 | if timestamp: 34 | post_date = datetime.datetime.fromtimestamp(timestamp).strftime('%F') 35 | # append date we're on to this file to get an idea of progress/time left 36 | if write_to_datefile: 37 | # optionally write the date of current post to a file 38 | with open('dateSearchIsOn', 'a') as f: 39 | f.write(post_date + "\n") 40 | 41 | # if any(['blueball' in lbody, 'blue ball' in lbody, 'dear white people' in lbody]): 42 | if any_keyword_in(keywords, body.lower()): 43 | items.append(post) 44 | return items 45 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/helpers.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | def validate_params(valid_options, params): 4 | """ 5 | Helps us validate the parameters for the request 6 | 7 | :param valid_options: a list of strings of valid options for the 8 | api request 9 | :param params: a dict, the key-value store which we really only care about 10 | the key which has tells us what the user is using for the 11 | API request 12 | 13 | :returns: None or throws an exception if the validation fails 14 | """ 15 | #crazy little if statement hanging by himself :( 16 | if not params: 17 | return 18 | 19 | #We only allow one version of the data parameter to be passed 20 | data_filter = ['data', 'source', 'external_url', 'embed'] 21 | multiple_data = [key for key in params.keys() if key in data_filter] 22 | if len(multiple_data) > 1: 23 | raise Exception("You can't mix and match data parameters") 24 | 25 | #No bad fields which are not in valid options can pass 26 | disallowed_fields = [key for key in params.keys() if key not in valid_options] 27 | if disallowed_fields: 28 | field_strings = ",".join(disallowed_fields) 29 | raise Exception("{0} are not allowed fields".format(field_strings)) 30 | 31 | def validate_blogname(fn): 32 | """ 33 | Decorator to validate the blogname and let you pass in a blogname like: 34 | client.blog_info('codingjester') 35 | or 36 | client.blog_info('codingjester.tumblr.com') 37 | or 38 | client.blog_info('blog.johnbunting.me') 39 | 40 | and query all the same blog. 41 | """ 42 | @wraps(fn) 43 | def add_dot_tumblr(*args, **kwargs): 44 | if (len(args) > 1 and ("." not in args[1])): 45 | args = list(args) 46 | args[1] += ".tumblr.com" 47 | return fn(*args, **kwargs) 48 | return add_dot_tumblr 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## plugin.video.tumblrV 2 | ### Kodi/XBMC Addon for Video's on Tumblr 3 | This is an in development addon to watch and download video's from tumblr blogs. 4 | ### GayMods Repo for Kodi/XBMC Gay Adult Addons 5 | **https://github.com/moedje/kodi-repo-gaymods** 6 | 7 | ### REQUIRED TO WORK: OAuth from Tumblr 8 | You need to authorize the app with Tumblr to your account so we can retrieve your following, liked, dashboard, etc. I do not have an easy way to do this in Kodi yet and I am working on this. For now you have to do this with a browser that you are logged into tumblr simply visit: 9 | 10 | #### https://api.tumblr.com/console/calls/user/info 11 | - Consumer Key: 12 | **5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4** 13 | - Consumer Secret: 14 | **GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch** 15 | 16 | Tumblr will give you back an **OAUTH_TOKEN and OAUTH_SECRET** you need to put this into the addon's settings and then it will work. If anyone knows how to make this work easier from within Kodi that would be most helpful!! 17 | ### Features and Status 18 | *updated 7 May 2017* 19 | 20 | - [x] OAuth2 login to Tumblr: Give URL to enter displayed CONSUMER KEY and SECRET to generate OAUTH Token and Secret 21 | - [ ] OAuth2 Can we make this easier to get OAUTH Token and Secret? Within Kodi and not browser? 22 | - [x] Dashboard Video's 23 | - [x] List of Following Blogs 24 | - [x] List of Video's from a blog 25 | - [x] List More/Older Video's from a blog 26 | - [x] Collect TAGS from any video's as you use addon 27 | - [x] List all collected tags and display video's with this tag 28 | - [x] Display Liked Videos 29 | - [ ] Ability to Like/save a video 30 | - [ ] Download A Video 31 | - [ ] Download All Liked/Dash/Category Videos 32 | - [ ] Download based on dates 33 | - [ ] Search Tumblr / Search a Blog / Other types of searches? 34 | - [ ] Handle Picture Posts? 35 | 36 | ## ABOUT ME 37 | - Author: Jeremy j@alljer.com 38 | - http://www.2my.cc/ 39 | - Founder: [CryptoCoins.Com] Physical CryptoCurrency Worth Holding Onto -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 17 | 19 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | video 15 | 16 | 17 | all 18 | en 19 | plugin.video.tumblrV Kodi/XBMC Addon for watching Videos from Tumblr 20 | V0.9.7: Search is working now though loads all results so is slow. 0.9.6: First attempt at new OAuth Token retrieving. Search works but needs lots of work, following retrieves all, dashboard can paginate now. V0.9.5: Added Download Video with YouTube-DL and added LIKE video, and improved loading of Blog's thumbnail image/avatar. V0.9.3: First public preview most features working. http://www.github.com/moedje/ Read the instructions about how to get your OAUTH_TOKEN and OAUTH_SECRET from Tumblr. It is required to login to Tumblr from an external App like this one. [COLOR red]YOU must put your OAUTH Token and Secret in settings[/COLOR] This will not work if you do not visit below Tumblr URL and put this App's Consumer Key and Secret in to generate an OAUTH token! 21 | [COLOR red]https://api.tumblr.com/console/calls/user/info REQUIRED TO GET OAUTH TOKEN TO PUT IN SETTINGS[/COLOR] 22 | Consumer Key: [B]5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4[/B] 23 | Consumer Secret: [B]GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch[/B] 24 | https://github.com/moedje/TumblrVideos 25 | 26 | May contain adult content so you must be of legal age in your jurisdiction to use tumblrV to view adult content. 27 | 28 | -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/addon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | video 15 | 16 | 17 | all 18 | en 19 | plugin.video.tumblrV Kodi/XBMC Addon for watching Videos from Tumblr 20 | V0.9.7: Search is working now though loads all results so is slow. 0.9.6: First attempt at new OAuth Token retrieving. Search works but needs lots of work, following retrieves all, dashboard can paginate now. V0.9.5: Added Download Video with YouTube-DL and added LIKE video, and improved loading of Blog's thumbnail image/avatar. V0.9.3: First public preview most features working. http://www.github.com/moedje/ Read the instructions about how to get your OAUTH_TOKEN and OAUTH_SECRET from Tumblr. It is required to login to Tumblr from an external App like this one. [COLOR red]YOU must put your OAUTH Token and Secret in settings[/COLOR] This will not work if you do not visit below Tumblr URL and put this App's Consumer Key and Secret in to generate an OAUTH token! 21 | [COLOR red]https://api.tumblr.com/console/calls/user/info REQUIRED TO GET OAUTH TOKEN TO PUT IN SETTINGS[/COLOR] 22 | Consumer Key: [B]5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4[/B] 23 | Consumer Secret: [B]GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch[/B] 24 | https://github.com/moedje/TumblrVideos 25 | 26 | May contain adult content so you must be of legal age in your jurisdiction to use tumblrV to view adult content. 27 | 28 | -------------------------------------------------------------------------------- /addons.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | video 16 | 17 | 18 | all 19 | en 20 | plugin.video.tumblrV Kodi/XBMC Addon for watching Videos from Tumblr 21 | V0.9.7: Search is working now though loads all results so is slow. 0.9.6: First attempt at new OAuth Token retrieving. Search works but needs lots of work, following retrieves all, dashboard can paginate now. V0.9.5: Added Download Video with YouTube-DL and added LIKE video, and improved loading of Blog's thumbnail image/avatar. V0.9.3: First public preview most features working. http://www.github.com/moedje/ Read the instructions about how to get your OAUTH_TOKEN and OAUTH_SECRET from Tumblr. It is required to login to Tumblr from an external App like this one. [COLOR red]YOU must put your OAUTH Token and Secret in settings[/COLOR] This will not work if you do not visit below Tumblr URL and put this App's Consumer Key and Secret in to generate an OAUTH token! 22 | [COLOR red]https://api.tumblr.com/console/calls/user/info REQUIRED TO GET OAUTH TOKEN TO PUT IN SETTINGS[/COLOR] 23 | Consumer Key: [B]5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4[/B] 24 | Consumer Secret: [B]GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch[/B] 25 | https://github.com/moedje/TumblrVideos 26 | 27 | May contain adult content so you must be of legal age in your jurisdiction to use tumblrV to view adult content. 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/__init__.py: -------------------------------------------------------------------------------- 1 | import urllib, urllib2, time, random, hmac, base64, hashlib 2 | from pytumblr import TumblrRestClient 3 | 4 | TUMBLRAPI = {'site': 'http://www.tumblr.com', 'request_token_url': "http://www.tumblr.com/oauth/request_token", 5 | 'authorize_url': "http://www.tumblr.com/oauth/authorize", 'token_url': "http://www.tumblr.com/oauth/access_token", 6 | 'callback_url': 'https://127.0.0.1/callback'} 7 | TUMBLRAUTH = dict(consumer_key='5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4', 8 | consumer_secret='GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch', 9 | oauth_token='', 10 | oauth_secret='') 11 | 12 | def makenonce(): 13 | random_number = ''.join( str( random.randint( 0, 9 ) ) for _ in range( 40 ) ) 14 | m = hashlib.md5( str( time.time() ) + str( random_number ) ) 15 | return m.hexdigest() 16 | 17 | def encodeparams(s): 18 | return urllib.quote( str( s ), safe='~' ) 19 | 20 | def getoauth(consumer_key = "5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4", consumer_secret = "GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch"): # oauth_consumer_secret 21 | request_tokenURL = 'http://www.tumblr.com/oauth/request_token' 22 | oauth_parameters = { 23 | 'oauth_consumer_key' : consumer_key, 24 | 'oauth_nonce' : makenonce(), 25 | 'oauth_timestamp' : str(int(time.time())), 26 | 'oauth_signature_method' : "HMAC-SHA1", 27 | 'oauth_version' : "1.0" 28 | } 29 | normalized_parameters = encodeparams( '&'.join( ['%s=%s' % ( encodeparams( str( k ) ), encodeparams( str( oauth_parameters[k] ) ) ) for k in sorted( oauth_parameters )] ) ) 30 | normalized_http_method = 'GET' 31 | normalized_http_url = encodeparams( request_tokenURL ) 32 | signature_base_string = '&'.join( [normalized_http_method, normalized_http_url, normalized_parameters] ) 33 | oauth_key = consumer_secret + '&' 34 | hashed = hmac.new( oauth_key, signature_base_string, hashlib.sha1 ) 35 | oauth_parameters['oauth_signature'] = base64.b64encode( hashed.digest() ) 36 | oauth_header = 'OAuth realm="http://www.tumblr.com",' + 'oauth_nonce="' + oauth_parameters['oauth_nonce'] + '",' + 'oauth_timestamp="' + oauth_parameters['oauth_timestamp'] + '",' + 'oauth_consumer_key="' + oauth_parameters['oauth_consumer_key'] + '",' + 'oauth_signature_method="HMAC-SHA1",oauth_version="1.0",oauth_signature="' + oauth_parameters['oauth_signature'] +'"' 37 | 38 | req = urllib2.Request( request_tokenURL ) 39 | req.add_header( 'Authorization', oauth_header ) 40 | tokenstr = urllib2.urlopen( req ).read() 41 | tokens = {} 42 | for token in tokenstr.split('&'): 43 | tname, tval = urllib.splitvalue(token) 44 | tokens.update({tname: tval}) 45 | TUMBLRAUTH.update({'oauth_token': tokens.get('oauth_token', ''), 'oauth_secret': tokens.get('oauth_token_secret', '')}) 46 | return TUMBLRAUTH 47 | 48 | # tclient = TumblrRestClient(**TUMBLRAUTH) 49 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/request.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | import urllib2 3 | import time 4 | import json 5 | import os 6 | try: 7 | from httplib2 import socks 8 | except ImportError: 9 | try: 10 | import socks 11 | except (ImportError, AttributeError): 12 | socks = None 13 | from urlparse import parse_qsl 14 | from urlparse import urlparse 15 | import oauth2 as oauth 16 | from httplib2 import RedirectLimit 17 | from httplib2 import ProxyInfo 18 | import httplib2 19 | 20 | 21 | class TumblrRequest(object): 22 | """ 23 | A simple request object that lets us query the Tumblr API 24 | """ 25 | 26 | def __init__(self, consumer_key, consumer_secret="", oauth_token="", oauth_secret="", host="https://api.tumblr.com", 27 | proxy_url=None): 28 | self.host = host 29 | self.consumer = oauth.Consumer(key=consumer_key, secret=consumer_secret) 30 | self.token = oauth.Token(key=oauth_token, secret=oauth_secret) 31 | self.proxy_url = proxy_url 32 | if proxy_url: 33 | print("Generating Proxy From proxy_url") 34 | self.proxy_info = httplib2.proxy_info_from_url("https://" + proxy_url, 'http') 35 | self.proxy_info.proxy_rdns = True 36 | # uri = urlparse(proxy_url) 37 | # self.proxy_info = ProxyInfo(socks.PROXY_TYPE_HTTP,uri.hostname,uri.port,proxy_rdns=True) 38 | else: 39 | print("Generating proxy from ENV") 40 | proxy_url = os.environ.get('HTTPS_PROXY', None) 41 | if proxy_url: 42 | uri = urlparse(proxy_url) 43 | self.proxy_info = ProxyInfo(socks.PROXY_TYPE_HTTP, uri.hostname, uri.port, proxy_rdns=True) 44 | else: 45 | self.proxy_info = None 46 | 47 | def get(self, url, params): 48 | """ 49 | Issues a GET request against the API, properly formatting the params 50 | 51 | :param url: a string, the url you are requesting 52 | :param params: a dict, the key-value of all the paramaters needed 53 | in the request 54 | :returns: a dict parsed of the JSON response 55 | """ 56 | url = self.host + url 57 | if params: 58 | url = url + "?" + urllib.urlencode(params) 59 | 60 | client = oauth.Client(self.consumer, self.token, proxy_info=self.proxy_info) 61 | client.disable_ssl_certificate_validation = True 62 | try: 63 | client.follow_redirects = False 64 | resp, content = client.request(url, method="GET", redirections=False) 65 | except RedirectLimit, e: 66 | resp, content = e.args 67 | 68 | return self.json_parse(content) 69 | 70 | def post(self, url, params={}, files=[]): 71 | """ 72 | Issues a POST request against the API, allows for multipart data uploads 73 | 74 | :param url: a string, the url you are requesting 75 | :param params: a dict, the key-value of all the parameters needed 76 | in the request 77 | :param files: a list, the list of tuples of files 78 | 79 | :returns: a dict parsed of the JSON response 80 | """ 81 | url = self.host + url 82 | try: 83 | if files: 84 | return self.post_multipart(url, params, files) 85 | else: 86 | client = oauth.Client(self.consumer, self.token, proxy_info=self.proxy_info) 87 | client.disable_ssl_certificate_validation = True 88 | resp, content = client.request(url, method="POST", body=urllib.urlencode(params)) 89 | return self.json_parse(content) 90 | except urllib2.HTTPError, e: 91 | return self.json_parse(e.read()) 92 | 93 | def json_parse(self, content): 94 | """ 95 | Wraps and abstracts content validation and JSON parsing 96 | to make sure the user gets the correct response. 97 | 98 | :param content: The content returned from the web request to be parsed as json 99 | 100 | :returns: a dict of the json response 101 | """ 102 | try: 103 | data = json.loads(content) 104 | except ValueError, e: 105 | data = {'meta': {'status': 500, 'msg': 'Server Error'}, 106 | 'response': {"error": "Malformed JSON or HTML was returned."}} 107 | 108 | # We only really care about the response if we succeed 109 | # and the error if we fail 110 | if data['meta']['status'] in [200, 201, 301]: 111 | return data['response'] 112 | else: 113 | return data 114 | 115 | def post_multipart(self, url, params, files): 116 | """ 117 | Generates and issues a multipart request for data files 118 | 119 | :param url: a string, the url you are requesting 120 | :param params: a dict, a key-value of all the parameters 121 | :param files: a list, the list of tuples for your data 122 | 123 | :returns: a dict parsed from the JSON response 124 | """ 125 | # combine the parameters with the generated oauth params 126 | params = dict(params.items() + self.generate_oauth_params().items()) 127 | faux_req = oauth.Request(method="POST", url=url, parameters=params) 128 | faux_req.sign_request(oauth.SignatureMethod_HMAC_SHA1(), self.consumer, self.token) 129 | params = dict(parse_qsl(faux_req.to_postdata())) 130 | 131 | content_type, body = self.encode_multipart_formdata(params, files) 132 | headers = {'Content-Type': content_type, 'Content-Length': str(len(body))} 133 | 134 | # Do a bytearray of the body and everything seems ok 135 | r = urllib2.Request(url, body, headers) 136 | if self.proxy_url: 137 | proxy = urllib2.ProxyHandler({'http': self.proxy_url, 'https': self.proxy_url}) 138 | opener = urllib2.build_opener(proxy) 139 | urllib2.install_opener(opener) 140 | content = urllib2.urlopen(r).read() 141 | return self.json_parse(content) 142 | 143 | def encode_multipart_formdata(self, fields, files): 144 | """ 145 | Properly encodes the multipart body of the request 146 | 147 | :param fields: a dict, the parameters used in the request 148 | :param files: a list of tuples containing information about the files 149 | 150 | :returns: the content for the body and the content-type value 151 | """ 152 | import mimetools 153 | import mimetypes 154 | BOUNDARY = mimetools.choose_boundary() 155 | CRLF = '\r\n' 156 | L = [] 157 | for (key, value) in fields.items(): 158 | L.append('--' + BOUNDARY) 159 | L.append('Content-Disposition: form-data; name="{0}"'.format(key)) 160 | L.append('') 161 | L.append(value) 162 | for (key, filename, value) in files: 163 | L.append('--' + BOUNDARY) 164 | L.append('Content-Disposition: form-data; name="{0}"; filename="{1}"'.format(key, filename)) 165 | L.append('Content-Type: {0}'.format(mimetypes.guess_type(filename)[0] or 'application/octet-stream')) 166 | L.append('Content-Transfer-Encoding: binary') 167 | L.append('') 168 | L.append(value) 169 | L.append('--' + BOUNDARY + '--') 170 | L.append('') 171 | body = CRLF.join(L) 172 | content_type = 'multipart/form-data; boundary={0}'.format(BOUNDARY) 173 | return content_type, body 174 | 175 | def generate_oauth_params(self): 176 | """ 177 | Generates the oauth parameters needed for multipart/form requests 178 | 179 | :returns: a dictionary of the proper headers that can be used 180 | in the request 181 | """ 182 | params = { 183 | 'oauth_version': "1.0", 184 | 'oauth_nonce': oauth.generate_nonce(), 185 | 'oauth_timestamp': int(time.time()), 186 | 'oauth_token': self.token.key, 187 | 'oauth_consumer_key': self.consumer.key 188 | } 189 | return params 190 | -------------------------------------------------------------------------------- /addons_xml_generator2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # * 3 | # * Copyright (C) 2012-2013 Garrett Brown 4 | # * Copyright (C) 2010 j48antialias 5 | # * 6 | # * This Program is free software; you can redistribute it and/or modify 7 | # * it under the terms of the GNU General Public License as published by 8 | # * the Free Software Foundation; either version 2, or (at your option) 9 | # * any later version. 10 | # * 11 | # * This Program is distributed in the hope that it will be useful, 12 | # * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # * GNU General Public License for more details. 15 | # * 16 | # * You should have received a copy of the GNU General Public License 17 | # * along with XBMC; see the file COPYING. If not, write to 18 | # * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA. 19 | # * http://www.gnu.org/copyleft/gpl.html 20 | # * 21 | # * Based on code by j48antialias: 22 | # * https://anarchintosh-projects.googlecode.com/files/addons_xml_generator.py 23 | 24 | """ addons.xml generator """ 25 | 26 | import os 27 | import sys 28 | import time 29 | import re 30 | import xml.etree.ElementTree as ET 31 | try: 32 | import shutil, zipfile 33 | except Exception as e: 34 | print('An error occurred importing module!\n%s\n' % e) 35 | 36 | # Compatibility with 3.0, 3.1 and 3.2 not supporting u"" literals 37 | print(sys.version) 38 | if sys.version < '3': 39 | import codecs 40 | def u(x): 41 | return codecs.unicode_escape_decode(x)[0] 42 | else: 43 | def u(x): 44 | return x 45 | 46 | class Generator: 47 | """ 48 | Generates a new addons.xml file from each addons addon.xml file 49 | and a new addons.xml.md5 hash file. Must be run from the root of 50 | the checked-out repo. Only handles single depth folder structure. 51 | """ 52 | def __init__(self): 53 | # generate files 54 | self._generate_addons_file() 55 | self._generate_md5_file() 56 | # notify user 57 | print("Finished updating addons xml and md5 files\n") 58 | 59 | def _generate_addons_file(self): 60 | # addon list 61 | addons = os.listdir(".") 62 | # final addons text 63 | addons_xml = u("\n\n") 64 | # loop thru and add each addons addon.xml file 65 | for addon in addons: 66 | try: 67 | # skip any file or .svn folder or .git folder 68 | if (not os.path.isdir(addon) or addon == ".svn" or addon == ".git" or addon == "zips"): continue 69 | # create path 70 | _path = os.path.join(addon, "addon.xml") 71 | # split lines for stripping 72 | xml_lines = open(_path, "r").read().splitlines() 73 | # new addon 74 | addon_xml = "" 75 | # loop thru cleaning each line 76 | for line in xml_lines: 77 | # skip encoding format line 78 | if (line.find("= 0): continue 79 | # add line 80 | if sys.version < '3': 81 | addon_xml += unicode(line.rstrip() + "\n", "UTF-8") 82 | else: 83 | addon_xml += line.rstrip() + "\n" 84 | # we succeeded so add to our final addons.xml text 85 | addons_xml += addon_xml.rstrip() + "\n\n" 86 | except Exception as e: 87 | # missing or poorly formatted addon.xml 88 | print("Excluding %s for %s" % (_path, e)) 89 | # clean and add closing tag 90 | addons_xml = addons_xml.strip() + u("\n\n") 91 | # save file 92 | self._save_file(addons_xml.encode("UTF-8"), file="addons.xml") 93 | 94 | def _generate_md5_file(self): 95 | # create a new md5 hash 96 | try: 97 | import md5 98 | m = md5.new(open("addons.xml", "r").read()).hexdigest() 99 | except ImportError: 100 | import hashlib 101 | m = hashlib.md5(open("addons.xml", "r", encoding="UTF-8").read().encode("UTF-8")).hexdigest() 102 | 103 | # save file 104 | try: 105 | self._save_file(m.encode("UTF-8"), file="addons.xml.md5") 106 | except Exception as e: 107 | # oops 108 | print("An error occurred creating addons.xml.md5 file!\n%s" % e) 109 | 110 | def _save_file(self, data, file): 111 | try: 112 | # write data to the file (use b for Python 3) 113 | open(file, "wb").write(data) 114 | except Exception as e: 115 | # oops 116 | print("An error occurred saving %s file!\n%s" % (file, e)) 117 | 118 | 119 | def zipfolder(foldername, target_dir, zips_dir, addon_dir): 120 | zipobj = zipfile.ZipFile(zips_dir + foldername, 'w', zipfile.ZIP_DEFLATED) 121 | rootlen = len(target_dir) + 1 122 | for base, dirs, files in os.walk(target_dir): 123 | for f in files: 124 | fn = os.path.join(base, f) 125 | zipobj.write(fn, os.path.join(addon_dir, fn[rootlen:])) 126 | zipobj.close() 127 | 128 | 129 | 130 | if (__name__ == "__main__"): 131 | # start 132 | Generator() 133 | 134 | # rezip files and move 135 | try: 136 | print('Starting zip file creation...') 137 | rootdir = sys.path[0] 138 | zipsdir = rootdir + os.sep + 'zips' 139 | filesinrootdir = os.listdir(rootdir) 140 | 141 | for x in filesinrootdir: 142 | if re.search("^(context|plugin|script|service|skin|repository)" , x) and not re.search('.zip', x): 143 | zipfilename = x + '.zip' 144 | zipfilenamefirstpart = zipfilename[:-4] 145 | zipfilenamelastpart = zipfilename[len(zipfilename) - 4:] 146 | zipsfolder = os.path.normpath(os.path.join('zips', x)) + os.sep 147 | foldertozip = rootdir + os.sep + x 148 | filesinfoldertozip = os.listdir(foldertozip) 149 | # #check if zips folder exists 150 | if not os.path.exists(zipsfolder): 151 | os.makedirs(zipsfolder) 152 | print('Directory doesn\'t exist, creating: ' + zipsfolder) 153 | # #get addon version number 154 | if "addon.xml" in filesinfoldertozip: 155 | tree = ET.parse(os.path.join(rootdir, x, "addon.xml")) 156 | root = tree.getroot() 157 | for elem in root.iter('addon'): 158 | print('%s %s version: %s' % (x, elem.tag, elem.attrib['version'])) 159 | version = '-' + elem.attrib['version'] 160 | # #check if and move addon, changelog, fanart and icon to zipdir 161 | for y in filesinfoldertozip: 162 | # print('processing file: ' + os.path.join(rootdir,x,y)) 163 | if re.search("addon|changelog|icon|fanart", y): 164 | shutil.copyfile(os.path.join(rootdir, x, y), os.path.join(zipsfolder, y)) 165 | print('Copying %s to %s' % (y, zipsfolder)) 166 | # #check for and zip the folders 167 | print('Zipping %s and moving to %s\n' % (x, zipsfolder)) 168 | zfilename = zipfilenamefirstpart + version + zipfilenamelastpart 169 | try: 170 | zipfolder(zfilename, foldertozip, zipsfolder, x) 171 | print('zipped with zipfolder\n') 172 | except: 173 | if os.path.exists(zipsfolder + x + version + '.zip'): 174 | os.remove(zipsfolder + x + version + '.zip') 175 | print('trying shutil') 176 | try: 177 | shutil.move(shutil.make_archive(foldertozip + version, 'zip', rootdir, x), zipsfolder) 178 | print('zipped with shutil\n') 179 | except Exception as e: 180 | print('Cannot create zip file\nshutil %s\n' % e) 181 | fpath = os.path.join(rootdir, zipsfolder, zfilename) 182 | shutil.copyfile(fpath, fpath.replace("zips/","")) 183 | fpath = fpath.replace(rootdir, "") 184 | print('Copying .{0} to .{1}'.format(fpath, fpath.replace("zips/",""))) 185 | except Exception as e: 186 | print('Cannot create or move the needed files\n%s' % e) 187 | print('Done') 188 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/tumblr.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright 2008 Ryan Cox ( ryan.a.cox@gmail.com ) All Rights Reserved. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # http://www.tumblr.com/docs/en/api/v1 18 | # 19 | 20 | '''A wrapper library for Tumblr's public web API: http://www.tumblr.com/api''' 21 | 22 | __author__ = 'ryan.a.cox@gmail.com' 23 | __version__ = '0.1' 24 | 25 | from httplib import HTTPConnection 26 | from urllib2 import Request, urlopen, URLError, HTTPError 27 | from urllib import urlencode, quote 28 | #from poster.encode import multipart_encode 29 | #from poster.streaminghttp import register_openers 30 | 31 | import base64 32 | import re 33 | 34 | try: 35 | import simplejson 36 | except ImportError: 37 | import json # from django.utils import simplejson 38 | 39 | 40 | 41 | GENERATOR = 'python-tumblr' 42 | PAGESIZE = 50 43 | 44 | 45 | class TumblrError(Exception): 46 | ''' General Tumblr error ''' 47 | def __init__(self, msg): 48 | self.msg = msg 49 | 50 | def __str__(self): 51 | return self.msg 52 | 53 | class TumblrAuthError(TumblrError): 54 | ''' Wraps a 403 result ''' 55 | pass 56 | 57 | class TumblrRequestError(TumblrError): 58 | ''' Wraps a 400 result ''' 59 | pass 60 | 61 | class TumblrIterator(object): 62 | def __init__(self, name, start, max, type, filter): 63 | self.name = name 64 | self.start = start 65 | self.max = max 66 | self.type = type 67 | self.results = None 68 | self.index = 0 69 | self.filter = filter 70 | 71 | def __iter__(self): 72 | return self 73 | 74 | def next(self): 75 | ''' 76 | Iterator explained: 77 | On initial run self.results will be empty and thus a service call is made to tumblr. This payload returned from the 78 | tumblr api will be parsed and stored inside of self.results. The len(self.results) is basically our total number of iterations 79 | to run for the specified elements returned for the given start/num sent to the intial api request. 80 | 81 | After the results are stored the code will increment the self.index and return an element 82 | 83 | Subsequent iterations will rotate through skipping another service call unless the index 84 | has caught up to the number of total posts, if so another request is made this time using 85 | the current index as the 'start' for the service call. Think of this as fetching the next 86 | page of results from tumblr. If there are no result left then self.results is going to 87 | be empty, and the StopIteration is going to be thrown when evaluated. 88 | 89 | ** Important, if some some reason 'start' is not passed correctly to the api, this will 90 | result in an infinite loop 91 | ''' 92 | if not self.results or (self.index == len(self.results['posts'])): 93 | self.start += self.index 94 | self.index = 0 95 | filter_url_param = '' 96 | url = "http://%s.tumblr.com/api/read/json?start=%s&num=%s" % (self.name,self.start, PAGESIZE) 97 | if self.type: 98 | url += "&type=" + self.type 99 | if self.filter != None: 100 | url += '&filter=%s'%self.filter 101 | response = urlopen(url) 102 | page = response.read() 103 | m = re.match("^.*?({.*}).*$", page,re.DOTALL | re.MULTILINE | re.UNICODE) 104 | self.results = simplejson.loads(m.group(1)) 105 | 106 | if (self.index >= self.max) or len(self.results['posts']) == 0: 107 | raise StopIteration 108 | 109 | self.index += 1 110 | return self.results['posts'][self.index-1] 111 | 112 | class TumblrIteratorAuthenticated(TumblrIterator): 113 | def __init__(self,name, email, password, start,max,type,filter): 114 | self.email = email 115 | self.password = password 116 | super(TumblrIteratorAuthenticated, self).__init__(name,start,max,type,filter) 117 | 118 | def next(self): 119 | ''' 120 | See above for initial explanation of iterator 121 | 122 | Additional Notes: 123 | 124 | urlopen(url,params) - by passing params to urlopen it makes the request POST *required* 125 | for authenticated_read 126 | 127 | This authenticated fetches the json data stream from tumblr. Authenticated mode means 128 | private items are returned, problem with json is it doesn't indicate which entries are 129 | private. The xml version of the data stream contains an attribute on the post node. 130 | So tumblr needs to add this property to the json for this to work properply. 131 | 132 | ''' 133 | if not self.results or (self.index == len(self.results['posts'])): 134 | self.start += self.index 135 | self.index = 0 136 | ## 137 | ## Only send email/pwd through post, all other params MUST be get values otherwise 138 | ## they will be ignored by tumblr api 139 | ## 140 | url = "http://%s.tumblr.com/api/read/json?start=%s&num=%s" % (self.name,self.start, PAGESIZE) 141 | param_set = {'password':self.password, 'email':self.email} 142 | if self.type: 143 | url += "&type=" + self.type 144 | if self.filter != None: 145 | url += '&filter=%s'%self.filter 146 | ## need to encode params for url open to do POST for authenticated read 147 | params = urlencode(param_set) 148 | response = urlopen(url, params) 149 | page = response.read() 150 | m = re.match("^.*?({.*}).*$", page,re.DOTALL | re.MULTILINE | re.UNICODE) 151 | self.results = simplejson.loads(m.group(1)) 152 | 153 | if (self.index >= self.max) or len(self.results['posts']) == 0: 154 | raise StopIteration 155 | 156 | self.index += 1 157 | return self.results['posts'][self.index-1] 158 | 159 | 160 | class Api(object): 161 | def __init__(self, name, email=None, password=None, private=None, date=None, tags=None, format=None): 162 | self.name = name 163 | self.is_authenticated = False 164 | self.email = email 165 | self.password = password 166 | self.private = private 167 | self.date = date 168 | self.tags = tags 169 | self.format = format 170 | 171 | def auth_check(self): 172 | if self.is_authenticated: 173 | return 174 | url = 'http://www.tumblr.com/api/write' 175 | values = { 176 | 'action': 'authenticate', 177 | 'generator' : GENERATOR, 178 | 'email': self.email, 179 | 'password' : self.password, 180 | 'private' : self.private, 181 | 'group': self.name, 182 | 'date': self.date, 183 | 'tags': self.tags, 184 | 'format': self.format 185 | } 186 | 187 | data = urlencode(values) 188 | req = Request(url, data) 189 | try: 190 | response = urlopen(req) 191 | page = response.read() 192 | self.url = page 193 | self.is_authenticated = True 194 | return 195 | except HTTPError, e: 196 | if 403 == e.code: 197 | raise TumblrAuthError(str(e)) 198 | if 400 == e.code: 199 | raise TumblrRequestError(str(e)) 200 | except Exception, e: 201 | raise TumblrError(str(e)) 202 | 203 | def dashboard(self): 204 | self.domain = 'http://www.tumblr.com' 205 | self.url = self.domain + '/login' 206 | self.params = urlencode({'email':self.email, 'password': self.password}) 207 | self.headers = {"Content-type": "application/x-www-form-urlencoded", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"} 208 | self.response = self._getcookie(self.domain, self.url, self.headers, self.params) 209 | 210 | self.cookie = self._cookie(self.response) 211 | 212 | self.response = self._getcookie(self.domain, self.url, self.headers, self.params, self.cookie) 213 | self.url_iphone = 'http://www.tumblr.com/iphone' 214 | self.data = self._getcookie(self.domain, self.url_iphone, self.headers, self.params, self.cookie) 215 | print self.data.read() 216 | 217 | def _cookie(self, response): 218 | self.cookie = response.getheader('set-cookie') 219 | 220 | self.pfu = self.cookie[self.cookie.find('pfu'):self.cookie.find(' ')] 221 | self.pfp = self.cookie[self.cookie.find('pfp'):] 222 | self.pfp = self.pfp[:self.pfp.find(' ')] 223 | self.pfe = self.cookie[self.cookie.find('pfe'):] 224 | self.pfe = self.pfe[:self.pfe.find(' ')] 225 | self.cookie = self.pfu + self.pfp + self.pfe 226 | 227 | return self.cookie 228 | 229 | 230 | def _getcookie(self, domain, url, headers, params = None, cookie = None): 231 | self.session = HTTPConnection(domain, '80') 232 | if cookie: 233 | headers['Cookie'] = cookie 234 | #headers['Referer'] = 'http://www.tumblr.com/iphone' 235 | self.session.request('POST',url, params, headers) 236 | 237 | self.response = self.session.getresponse() 238 | #print self.response.status, self.response.reason 239 | return self.response 240 | 241 | def write_regular(self, title=None, body=None, **args): 242 | if title: 243 | args['title'] = title 244 | if body: 245 | args['body'] = body 246 | args = self._fixnames(args) 247 | if not 'title' in args and not 'body' in args: 248 | raise TumblrError("Must supply either body or title argument") 249 | 250 | self.auth_check() 251 | args['type'] = 'regular' 252 | return self._write(args) 253 | 254 | def write_photo(self, source=None, data=None, caption=None, click=None, **args): 255 | if source: 256 | args['source'] = source 257 | else: 258 | args['data'] = open(data) 259 | 260 | args['caption'] = caption 261 | args['click-through-url'] = click 262 | 263 | args = self._fixnames(args) 264 | if 'source' in args and 'data' in args: 265 | raise TumblrError("Must NOT supply both source and data arguments") 266 | 267 | if not 'source' in args and not 'data' in args: 268 | raise TumblrError("Must supply source or data argument") 269 | 270 | self.auth_check() 271 | args['type'] = 'photo' 272 | return self._write(args) 273 | 274 | def write_quote(self, quote=None, source=None, **args): 275 | if quote: 276 | args['quote'] = quote 277 | args['source'] = source 278 | args = self._fixnames(args) 279 | if not 'quote' in args: 280 | raise TumblrError("Must supply quote arguments") 281 | 282 | self.auth_check() 283 | args['type'] = 'quote' 284 | return self._write(args) 285 | 286 | def write_link(self, name=None, url=None, description=None, **args): 287 | if url: 288 | args['name'] = name 289 | args['url'] = url 290 | args['description'] = description 291 | args = self._fixnames(args) 292 | if not 'url' in args: 293 | raise TumblrError("Must supply url argument") 294 | 295 | self.auth_check() 296 | args['type'] = 'link' 297 | return self._write(args) 298 | 299 | def write_conversation(self, title=None, conversation=None, **args): 300 | if conversation: 301 | args['title'] = title 302 | args['conversation'] = conversation 303 | args = self._fixnames(args) 304 | if not 'conversation' in args: 305 | raise TumblrError("Must supply conversation argument") 306 | 307 | self.auth_check() 308 | args['type'] = 'conversation' 309 | return self._write(args) 310 | 311 | def write_audio(self, data=None, source=None, caption=None, **args): 312 | if data: 313 | args['data'] = open(data) 314 | else: 315 | args['data'] = urlopen(source).read() 316 | 317 | args['caption'] = caption 318 | args = self._fixnames(args) 319 | 320 | if not 'data' in args: 321 | raise TumblrError("Must supply data argument") 322 | 323 | self.auth_check() 324 | args['type'] = 'audio' 325 | return self._write(args) 326 | 327 | def write_video(self, embed=None, caption=None, **args): 328 | if embed: 329 | args['embed'] = embed 330 | args['caption'] = caption 331 | args = self._fixnames(args) 332 | if 'embed' in args and 'data' in args: 333 | raise TumblrError("Must NOT supply both embed and data arguments") 334 | 335 | if not 'embed' in args and not 'data' in args: 336 | raise TumblrError("Must supply embed or data argument") 337 | 338 | self.auth_check() 339 | args['type'] = 'video' 340 | return self._write(args) 341 | 342 | def _fixnames(self, args): 343 | for key in args: 344 | if '_' in key: 345 | value = args[key] 346 | del args[key] 347 | args[key.replace('_', '-')] = value 348 | return args 349 | 350 | def _write(self, params, headers=None): 351 | self.auth_check() 352 | url = 'http://www.tumblr.com/api/write' 353 | #register_openers() 354 | params['email'] = self.email 355 | params['password'] = self.password 356 | params['private'] = self.private 357 | params['generator'] = GENERATOR 358 | params['group'] = self.name 359 | params['date'] = self.date 360 | params['tags'] = self.tags 361 | params['format'] = self.format 362 | 363 | if not params['date']: 364 | params['date'] = 'now' 365 | if not params['tags']: 366 | del params['tags'] 367 | if not params['format']: 368 | del params['format'] 369 | 370 | if not 'data' in params: 371 | data = urlencode(params) 372 | else: 373 | data, headers = None, None# multipart_encode(params) 374 | 375 | if headers: 376 | req = Request(url, data, headers) 377 | else: 378 | req = Request(url, data) 379 | 380 | newid = None 381 | #print params 382 | try: 383 | f = urlopen(req) 384 | raise TumblrError("Error writing post") 385 | 386 | except HTTPError, e: 387 | if 201 == e.code: 388 | newid = e.read() 389 | return self.read(id=newid) 390 | raise TumblrError(e.read()) 391 | 392 | def authenticated_read(self, id=None, start=0, max=2**31-1, type=None, filter=None): 393 | ''' 394 | a close of the read method only it uses post instead and includes email/password 395 | to authenticate the read. note it returns a subclasses tumblr-iterator 396 | ''' 397 | 398 | if id: 399 | url = "http://%s.tumblr.com/api/read/json" % (self.name) 400 | ## need to encode params for urlopen to do POST for authenticated read 401 | params = urlencode({'email':self.email, 'password':self.password, 'start':start, 'id':id}) 402 | response = urlopen(url=url, data=params) 403 | page = response.read() 404 | m = re.match("^.*?({.*}).*$", page,re.DOTALL | re.MULTILINE | re.UNICODE) 405 | results = simplejson.loads(m.group(1)) 406 | if len(results['posts']) == 0: 407 | return None 408 | return results['posts'][0] 409 | else: 410 | return TumblrIteratorAuthenticated(self.name,self.email,self.password,start,max,type, filter) 411 | 412 | 413 | def read(self, id=None, start=0,max=2**31-1,type=None, filter=None): 414 | 415 | if id: 416 | filter_url_param = '' 417 | if filter != None: 418 | filter_url_param = '&filter=%s' % filter 419 | url = "http://%s.tumblr.com/api/read/json?id=%s" % (self.name,id) 420 | response = urlopen(url) 421 | page = response.read() 422 | m = re.match("^.*?({.*}).*$", page,re.DOTALL | re.MULTILINE | re.UNICODE) 423 | results = simplejson.loads(m.group(1)) 424 | if len(results['posts']) == 0: 425 | return None 426 | 427 | return results['posts'][0] 428 | else: 429 | return TumblrIterator(self.name,start,max,type,filter) 430 | 431 | if __name__ == "__main__": 432 | pass 433 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/__init__.py: -------------------------------------------------------------------------------- 1 | from helpers import validate_params, validate_blogname 2 | from request import TumblrRequest 3 | 4 | 5 | class TumblrRestClient(object): 6 | """ 7 | A Python Client for the Tumblr API 8 | """ 9 | 10 | def __init__(self, consumer_key, consumer_secret="", oauth_token="", oauth_secret="", host="https://api.tumblr.com",proxy_url=None): 11 | """ 12 | Initializes the TumblrRestClient object, creating the TumblrRequest 13 | object which deals with all request formatting. 14 | 15 | :param consumer_key: a string, the consumer key of your 16 | Tumblr Application 17 | :param consumer_secret: a string, the consumer secret of 18 | your Tumblr Application 19 | :param oauth_token: a string, the user specific token, received 20 | from the /access_token endpoint 21 | :param oauth_secret: a string, the user specific secret, received 22 | from the /access_token endpoint 23 | :param host: the host that are you trying to send information to, 24 | defaults to http://api.tumblr.com 25 | 26 | :returns: None 27 | """ 28 | self.request = TumblrRequest(consumer_key, consumer_secret, oauth_token, oauth_secret, host,proxy_url=proxy_url) 29 | 30 | def info(self): 31 | """ 32 | Gets the information about the current given user 33 | 34 | :returns: A dict created from the JSON response 35 | """ 36 | return self.send_api_request("get", "/v2/user/info") 37 | 38 | @validate_blogname 39 | def avatar(self, blogname, size=64): 40 | """ 41 | Retrieves the url of the blog's avatar 42 | 43 | :param blogname: a string, the blog you want the avatar for 44 | 45 | :returns: A dict created from the JSON response 46 | """ 47 | url = "/v2/blog/{0}/avatar/{1}".format(blogname, size) 48 | return self.send_api_request("get", url) 49 | 50 | def likes(self, **kwargs): 51 | """ 52 | Gets the current given user's likes 53 | :param limit: an int, the number of likes you want returned 54 | :param offset: an int, the like you want to start at, for pagination. 55 | 56 | # Start at the 20th like and get 20 more likes. 57 | client.likes({'offset': 20, 'limit': 20}) 58 | 59 | :returns: A dict created from the JSON response 60 | """ 61 | return self.send_api_request("get", "/v2/user/likes", kwargs, ["limit", "offset"]) 62 | 63 | def following(self, **kwargs): 64 | """ 65 | Gets the blogs that the current user is following. 66 | :param limit: an int, the number of likes you want returned 67 | :param offset: an int, the blog you want to start at, for pagination. 68 | 69 | # Start at the 20th blog and get 20 more blogs. 70 | client.following({'offset': 20, 'limit': 20}) 71 | 72 | :returns: A dict created from the JSON response 73 | """ 74 | return self.send_api_request("get", "/v2/user/following", kwargs, ["limit", "offset"]) 75 | 76 | def dashboard(self, **kwargs): 77 | """ 78 | Gets the dashboard of the current user 79 | 80 | :param limit: an int, the number of posts you want returned 81 | :param offset: an int, the posts you want to start at, for pagination. 82 | :param type: the type of post you want to return 83 | :param since_id: return only posts that have appeared after this ID 84 | :param reblog_info: return reblog information about posts 85 | :param notes_info: return notes information about the posts 86 | 87 | :returns: A dict created from the JSON response 88 | """ 89 | return self.send_api_request("get", "/v2/user/dashboard", kwargs, ["limit", "offset", "type", "since_id", "reblog_info", "notes_info"]) 90 | 91 | def tagged(self, tag, **kwargs): 92 | """ 93 | Gets a list of posts tagged with the given tag 94 | 95 | :param tag: a string, the tag you want to look for 96 | :param before: a unix timestamp, the timestamp you want to start at 97 | to look at posts. 98 | :param limit: the number of results you want 99 | :param filter: the post format that you want returned: html, text, raw 100 | 101 | client.tagged("gif", limit=10) 102 | 103 | :returns: a dict created from the JSON response 104 | """ 105 | kwargs.update({'tag': tag}) 106 | return self.send_api_request("get", '/v2/tagged', kwargs, ['before', 'limit', 'filter', 'tag', 'api_key'], True) 107 | 108 | @validate_blogname 109 | def posts(self, blogname, type=None, **kwargs): 110 | """ 111 | Gets a list of posts from a particular blog 112 | 113 | :param blogname: a string, the blogname you want to look up posts 114 | for. eg: codingjester.tumblr.com 115 | :param id: an int, the id of the post you are looking for on the blog 116 | :param tag: a string, the tag you are looking for on posts 117 | :param limit: an int, the number of results you want 118 | :param offset: an int, the offset of the posts you want to start at. 119 | :param filter: the post format you want returned: HTML, text or raw. 120 | :param type: the type of posts you want returned, e.g. video. If omitted returns all post types. 121 | 122 | :returns: a dict created from the JSON response 123 | """ 124 | if type is None: 125 | url = '/v2/blog/{0}/posts'.format(blogname) 126 | else: 127 | url = '/v2/blog/{0}/posts/{1}'.format(blogname,type) 128 | return self.send_api_request("get", url, kwargs, ['id', 'tag', 'limit', 'offset', 'reblog_info', 'notes_info', 'filter', 'api_key'], True) 129 | 130 | @validate_blogname 131 | def blog_info(self, blogname): 132 | """ 133 | Gets the information of the given blog 134 | 135 | :param blogname: the name of the blog you want to information 136 | on. eg: codingjester.tumblr.com 137 | 138 | :returns: a dict created from the JSON response of information 139 | """ 140 | url = "/v2/blog/{0}/info".format(blogname) 141 | return self.send_api_request("get", url, {}, ['api_key'], True) 142 | 143 | @validate_blogname 144 | def followers(self, blogname, **kwargs): 145 | """ 146 | Gets the followers of the given blog 147 | :param limit: an int, the number of followers you want returned 148 | :param offset: an int, the follower to start at, for pagination. 149 | 150 | # Start at the 20th blog and get 20 more blogs. 151 | client.followers({'offset': 20, 'limit': 20}) 152 | 153 | :returns: A dict created from the JSON response 154 | """ 155 | url = "/v2/blog/{0}/followers".format(blogname) 156 | return self.send_api_request("get", url, kwargs, ['limit', 'offset']) 157 | 158 | @validate_blogname 159 | def blog_likes(self, blogname, **kwargs): 160 | """ 161 | Gets the current given user's likes 162 | :param limit: an int, the number of likes you want returned 163 | :param offset: an int, the like you want to start at, for pagination. 164 | 165 | # Start at the 20th like and get 20 more likes. 166 | client.blog_likes({'offset': 20, 'limit': 20}) 167 | 168 | :returns: A dict created from the JSON response 169 | """ 170 | url = "/v2/blog/{0}/likes".format(blogname) 171 | return self.send_api_request("get", url, kwargs, ['limit', 'offset'], True) 172 | 173 | @validate_blogname 174 | def queue(self, blogname, **kwargs): 175 | """ 176 | Gets posts that are currently in the blog's queue 177 | 178 | :param limit: an int, the number of posts you want returned 179 | :param offset: an int, the post you want to start at, for pagination. 180 | :param filter: the post format that you want returned: HTML, text, raw. 181 | 182 | :returns: a dict created from the JSON response 183 | """ 184 | url = "/v2/blog/{0}/posts/queue".format(blogname) 185 | return self.send_api_request("get", url, kwargs, ['limit', 'offset', 'filter']) 186 | 187 | @validate_blogname 188 | def drafts(self, blogname, **kwargs): 189 | """ 190 | Gets posts that are currently in the blog's drafts 191 | :param filter: the post format that you want returned: HTML, text, raw. 192 | 193 | :returns: a dict created from the JSON response 194 | """ 195 | url = "/v2/blog/{0}/posts/draft".format(blogname) 196 | return self.send_api_request("get", url, kwargs, ['filter']) 197 | 198 | @validate_blogname 199 | def submission(self, blogname, **kwargs): 200 | """ 201 | Gets posts that are currently in the blog's queue 202 | 203 | :param offset: an int, the post you want to start at, for pagination. 204 | :param filter: the post format that you want returned: HTML, text, raw. 205 | 206 | :returns: a dict created from the JSON response 207 | """ 208 | url = "/v2/blog/{0}/posts/submission".format(blogname) 209 | return self.send_api_request("get", url, kwargs, ["offset", "filter"]) 210 | 211 | @validate_blogname 212 | def follow(self, blogname): 213 | """ 214 | Follow the url of the given blog 215 | 216 | :param blogname: a string, the blog url you want to follow 217 | 218 | :returns: a dict created from the JSON response 219 | """ 220 | url = "/v2/user/follow" 221 | return self.send_api_request("post", url, {'url': blogname}, ['url']) 222 | 223 | @validate_blogname 224 | def unfollow(self, blogname): 225 | """ 226 | Unfollow the url of the given blog 227 | 228 | :param blogname: a string, the blog url you want to follow 229 | 230 | :returns: a dict created from the JSON response 231 | """ 232 | url = "/v2/user/unfollow" 233 | return self.send_api_request("post", url, {'url': blogname}, ['url']) 234 | 235 | def like(self, id, reblog_key): 236 | """ 237 | Like the post of the given blog 238 | 239 | :param id: an int, the id of the post you want to like 240 | :param reblog_key: a string, the reblog key of the post 241 | 242 | :returns: a dict created from the JSON response 243 | """ 244 | url = "/v2/user/like" 245 | params = {'id': id, 'reblog_key': reblog_key} 246 | return self.send_api_request("post", url, params, ['id', 'reblog_key']) 247 | 248 | def unlike(self, id, reblog_key): 249 | """ 250 | Unlike the post of the given blog 251 | 252 | :param id: an int, the id of the post you want to like 253 | :param reblog_key: a string, the reblog key of the post 254 | 255 | :returns: a dict created from the JSON response 256 | """ 257 | url = "/v2/user/unlike" 258 | params = {'id': id, 'reblog_key': reblog_key} 259 | return self.send_api_request("post", url, params, ['id', 'reblog_key']) 260 | 261 | @validate_blogname 262 | def create_photo(self, blogname, **kwargs): 263 | """ 264 | Create a photo post or photoset on a blog 265 | 266 | :param blogname: a string, the url of the blog you want to post to. 267 | :param state: a string, The state of the post. 268 | :param tags: a list of tags that you want applied to the post 269 | :param tweet: a string, the customized tweet that you want 270 | :param date: a string, the GMT date and time of the post 271 | :param format: a string, sets the format type of the post. html or markdown 272 | :param slug: a string, a short text summary to the end of the post url 273 | :param caption: a string, the caption that you want applied to the photo 274 | :param link: a string, the 'click-through' url you want on the photo 275 | :param source: a string, the photo source url 276 | :param data: a string or a list of the path of photo(s) 277 | 278 | :returns: a dict created from the JSON response 279 | """ 280 | kwargs.update({"type": "photo"}) 281 | return self._send_post(blogname, kwargs) 282 | 283 | @validate_blogname 284 | def create_text(self, blogname, **kwargs): 285 | """ 286 | Create a text post on a blog 287 | 288 | :param blogname: a string, the url of the blog you want to post to. 289 | :param state: a string, The state of the post. 290 | :param tags: a list of tags that you want applied to the post 291 | :param tweet: a string, the customized tweet that you want 292 | :param date: a string, the GMT date and time of the post 293 | :param format: a string, sets the format type of the post. html or markdown 294 | :param slug: a string, a short text summary to the end of the post url 295 | :param title: a string, the optional title of a post 296 | :param body: a string, the body of the text post 297 | 298 | :returns: a dict created from the JSON response 299 | """ 300 | kwargs.update({"type": "text"}) 301 | return self._send_post(blogname, kwargs) 302 | 303 | @validate_blogname 304 | def create_quote(self, blogname, **kwargs): 305 | """ 306 | Create a quote post on a blog 307 | 308 | :param blogname: a string, the url of the blog you want to post to. 309 | :param state: a string, The state of the post. 310 | :param tags: a list of tags that you want applied to the post 311 | :param tweet: a string, the customized tweet that you want 312 | :param date: a string, the GMT date and time of the post 313 | :param format: a string, sets the format type of the post. html or markdown 314 | :param slug: a string, a short text summary to the end of the post url 315 | :param quote: a string, the full text of the quote 316 | :param source: a string, the cited source of the quote 317 | 318 | :returns: a dict created from the JSON response 319 | """ 320 | kwargs.update({"type": "quote"}) 321 | return self._send_post(blogname, kwargs) 322 | 323 | @validate_blogname 324 | def create_link(self, blogname, **kwargs): 325 | """ 326 | Create a link post on a blog 327 | 328 | :param blogname: a string, the url of the blog you want to post to. 329 | :param state: a string, The state of the post. 330 | :param tags: a list of tags that you want applied to the post 331 | :param tweet: a string, the customized tweet that you want 332 | :param date: a string, the GMT date and time of the post 333 | :param format: a string, sets the format type of the post. html or markdown 334 | :param slug: a string, a short text summary to the end of the post url 335 | :param title: a string, the title of the link 336 | :param url: a string, the url of the link you are posting 337 | :param description: a string, the description of the link you are posting 338 | 339 | :returns: a dict created from the JSON response 340 | """ 341 | kwargs.update({"type": "link"}) 342 | return self._send_post(blogname, kwargs) 343 | 344 | @validate_blogname 345 | def create_chat(self, blogname, **kwargs): 346 | """ 347 | Create a chat post on a blog 348 | 349 | :param blogname: a string, the url of the blog you want to post to. 350 | :param state: a string, The state of the post. 351 | :param tags: a list of tags that you want applied to the post 352 | :param tweet: a string, the customized tweet that you want 353 | :param date: a string, the GMT date and time of the post 354 | :param format: a string, sets the format type of the post. html or markdown 355 | :param slug: a string, a short text summary to the end of the post url 356 | :param title: a string, the title of the conversation 357 | :param converstaion: a string, the conversation you are posting 358 | 359 | :returns: a dict created from the JSON response 360 | """ 361 | kwargs.update({"type": "chat"}) 362 | return self._send_post(blogname, kwargs) 363 | 364 | @validate_blogname 365 | def create_audio(self, blogname, **kwargs): 366 | """ 367 | Create a audio post on a blog 368 | 369 | :param blogname: a string, the url of the blog you want to post to. 370 | :param state: a string, The state of the post. 371 | :param tags: a list of tags that you want applied to the post 372 | :param tweet: a string, the customized tweet that you want 373 | :param date: a string, the GMT date and time of the post 374 | :param format: a string, sets the format type of the post. html or markdown 375 | :param slug: a string, a short text summary to the end of the post url 376 | :param caption: a string, the caption for the post 377 | :param external_url: a string, the url of the audio you are uploading 378 | :param data: a string, the local filename path of the audio you are uploading 379 | 380 | :returns: a dict created from the JSON response 381 | """ 382 | kwargs.update({"type": "audio"}) 383 | return self._send_post(blogname, kwargs) 384 | 385 | @validate_blogname 386 | def create_video(self, blogname, **kwargs): 387 | """ 388 | Create a audio post on a blog 389 | 390 | :param blogname: a string, the url of the blog you want to post to. 391 | :param state: a string, The state of the post. 392 | :param tags: a list of tags that you want applied to the post 393 | :param tweet: a string, the customized tweet that you want 394 | :param date: a string, the GMT date and time of the post 395 | :param format: a string, sets the format type of the post. html or markdown 396 | :param slug: a string, a short text summary to the end of the post url 397 | :param caption: a string, the caption for the post 398 | :param embed: a string, the emebed code that you'd like to upload 399 | :param data: a string, the local filename path of the video you are uploading 400 | 401 | :returns: a dict created from the JSON response 402 | """ 403 | kwargs.update({"type": "video"}) 404 | return self._send_post(blogname, kwargs) 405 | 406 | @validate_blogname 407 | def reblog(self, blogname, **kwargs): 408 | """ 409 | Creates a reblog on the given blogname 410 | 411 | :param blogname: a string, the url of the blog you want to reblog to 412 | :param id: an int, the post id that you are reblogging 413 | :param reblog_key: a string, the reblog key of the post 414 | :param comment: a string, a comment added to the reblogged post 415 | 416 | :returns: a dict created from the JSON response 417 | """ 418 | url = "/v2/blog/{0}/post/reblog".format(blogname) 419 | 420 | valid_options = ['id', 'reblog_key', 'comment'] + self._post_valid_options(kwargs.get('type', None)) 421 | if 'tags' in kwargs and kwargs['tags']: 422 | # Take a list of tags and make them acceptable for upload 423 | kwargs['tags'] = ",".join(kwargs['tags']) 424 | return self.send_api_request('post', url, kwargs, valid_options) 425 | 426 | @validate_blogname 427 | def delete_post(self, blogname, id): 428 | """ 429 | Deletes a post with the given id 430 | 431 | :param blogname: a string, the url of the blog you want to delete from 432 | :param id: an int, the post id that you want to delete 433 | 434 | :returns: a dict created from the JSON response 435 | """ 436 | url = "/v2/blog/{0}/post/delete".format(blogname) 437 | return self.send_api_request('post', url, {'id': id}, ['id']) 438 | 439 | @validate_blogname 440 | def edit_post(self, blogname, **kwargs): 441 | """ 442 | Edits a post with a given id 443 | 444 | :param blogname: a string, the url of the blog you want to edit 445 | :param state: a string, the state of the post. published, draft, queue, or private. 446 | :param tags: a list of tags that you want applied to the post 447 | :param tweet: a string, the customized tweet that you want 448 | :param date: a string, the GMT date and time of the post 449 | :param format: a string, sets the format type of the post. html or markdown 450 | :param slug: a string, a short text summary to the end of the post url 451 | :param id: an int, the post id that you want to edit 452 | 453 | :returns: a dict created from the JSON response 454 | """ 455 | url = "/v2/blog/{0}/post/edit".format(blogname) 456 | 457 | if 'tags' in kwargs and kwargs['tags']: 458 | # Take a list of tags and make them acceptable for upload 459 | kwargs['tags'] = ",".join(kwargs['tags']) 460 | 461 | valid_options = ['id'] + self._post_valid_options(kwargs.get('type', None)) 462 | return self.send_api_request('post', url, kwargs, valid_options) 463 | 464 | # Parameters valid for /post, /post/edit, and /post/reblog. 465 | def _post_valid_options(self, post_type=None): 466 | # These options are always valid 467 | valid = ['type', 'state', 'tags', 'tweet', 'date', 'format', 'slug'] 468 | 469 | # Other options are valid on a per-post-type basis 470 | if post_type == 'text': 471 | valid += ['title', 'body'] 472 | elif post_type == 'photo': 473 | valid += ['caption', 'link', 'source', 'data'] 474 | elif post_type == 'quote': 475 | valid += ['quote', 'source'] 476 | elif post_type == 'link': 477 | valid += ['title', 'url', 'description'] 478 | elif post_type == 'chat': 479 | valid += ['title', 'conversation'] 480 | elif post_type == 'audio': 481 | valid += ['caption', 'external_url', 'data'] 482 | elif post_type == 'video': 483 | valid += ['caption', 'embed', 'data'] 484 | 485 | return valid 486 | 487 | def _send_post(self, blogname, params): 488 | """ 489 | Formats parameters and sends the API request off. Validates 490 | common and per-post-type parameters and formats your tags for you. 491 | 492 | :param blogname: a string, the blogname of the blog you are posting to 493 | :param params: a dict, the key-value of the parameters for the api request 494 | :param valid_options: a list of valid options that the request allows 495 | 496 | :returns: a dict parsed from the JSON response 497 | """ 498 | url = "/v2/blog/{0}/post".format(blogname) 499 | valid_options = self._post_valid_options(params.get('type', None)) 500 | 501 | if 'tags' in params: 502 | # Take a list of tags and make them acceptable for upload 503 | params['tags'] = ",".join(params['tags']) 504 | 505 | return self.send_api_request("post", url, params, valid_options) 506 | 507 | def send_api_request(self, method, url, params={}, valid_parameters=[], needs_api_key=False): 508 | """ 509 | Sends the url with parameters to the requested url, validating them 510 | to make sure that they are what we expect to have passed to us 511 | 512 | :param method: a string, the request method you want to make 513 | :param params: a dict, the parameters used for the API request 514 | :param valid_parameters: a list, the list of valid parameters 515 | :param needs_api_key: a boolean, whether or not your request needs an api key injected 516 | 517 | :returns: a dict parsed from the JSON response 518 | """ 519 | if needs_api_key: 520 | params.update({'api_key': self.request.consumer.key}) 521 | valid_parameters.append('api_key') 522 | 523 | files = [] 524 | if 'data' in params: 525 | if isinstance(params['data'], list): 526 | for idx, data in enumerate(params['data']): 527 | with open(data, 'rb') as f: 528 | files.append(('data['+str(idx)+']', data, f.read())) 529 | else: 530 | with open(params['data'], 'rb') as f: 531 | files = [('data', params['data'], f.read())] 532 | del params['data'] 533 | 534 | validate_params(valid_parameters, params) 535 | if method == "get": 536 | return self.request.get(url, params) 537 | else: 538 | return self.request.post(url, params, files) 539 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/resources/lib/pytumblr/oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | The MIT License 3 | 4 | Copyright (c) 2007-2010 Leah Culver, Joe Stump, Mark Paschal, Vic Fryzel 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | """ 24 | 25 | import base64 26 | import urllib 27 | import time 28 | import random 29 | import urlparse 30 | import hmac 31 | import binascii 32 | import httplib2 33 | import ssl 34 | ssl._create_default_https_context = ssl._create_unverified_context 35 | 36 | try: 37 | from urlparse import parse_qs 38 | parse_qs # placate pyflakes 39 | except ImportError: 40 | # fall back for Python 2.5 41 | from cgi import parse_qs 42 | 43 | try: 44 | from hashlib import sha1 45 | sha = sha1 46 | except ImportError: 47 | # hashlib was added in Python 2.5 48 | import sha 49 | 50 | import _version 51 | 52 | __version__ = _version.__version__ 53 | 54 | OAUTH_VERSION = '1.0' # Hi Blaine! 55 | HTTP_METHOD = 'GET' 56 | SIGNATURE_METHOD = 'PLAINTEXT' 57 | 58 | 59 | class Error(RuntimeError): 60 | """Generic exception class.""" 61 | 62 | def __init__(self, message='OAuth error occurred.'): 63 | self._message = message 64 | 65 | @property 66 | def message(self): 67 | """A hack to get around the deprecation errors in 2.6.""" 68 | return self._message 69 | 70 | def __str__(self): 71 | return self._message 72 | 73 | 74 | class MissingSignature(Error): 75 | pass 76 | 77 | 78 | def build_authenticate_header(realm=''): 79 | """Optional WWW-Authenticate header (401 error)""" 80 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 81 | 82 | 83 | def build_xoauth_string(url, consumer, token=None): 84 | """Build an XOAUTH string for use in SMTP/IMPA authentication.""" 85 | request = Request.from_consumer_and_token(consumer, token, 86 | "GET", url) 87 | 88 | signing_method = SignatureMethod_HMAC_SHA1() 89 | request.sign_request(signing_method, consumer, token) 90 | 91 | params = [] 92 | for k, v in sorted(request.iteritems()): 93 | if v is not None: 94 | params.append('%s="%s"' % (k, escape(v))) 95 | 96 | return "%s %s %s" % ("GET", url, ','.join(params)) 97 | 98 | 99 | def to_unicode(s): 100 | """ Convert to unicode, raise exception with instructive error 101 | message if s is not unicode, ascii, or utf-8. """ 102 | if not isinstance(s, unicode): 103 | if not isinstance(s, str): 104 | raise TypeError('You are required to pass either unicode or string here, not: %r (%s)' % (type(s), s)) 105 | try: 106 | s = s.decode('utf-8') 107 | except UnicodeDecodeError, le: 108 | raise TypeError('You are required to pass either a unicode object or a utf-8 string here. You passed a Python string object which contained non-utf-8: %r. The UnicodeDecodeError that resulted from attempting to interpret it as utf-8 was: %s' % (s, le,)) 109 | return s 110 | 111 | def to_utf8(s): 112 | return to_unicode(s).encode('utf-8') 113 | 114 | def to_unicode_if_string(s): 115 | if isinstance(s, basestring): 116 | return to_unicode(s) 117 | else: 118 | return s 119 | 120 | def to_utf8_if_string(s): 121 | if isinstance(s, basestring): 122 | return to_utf8(s) 123 | else: 124 | return s 125 | 126 | def to_unicode_optional_iterator(x): 127 | """ 128 | Raise TypeError if x is a str containing non-utf8 bytes or if x is 129 | an iterable which contains such a str. 130 | """ 131 | if isinstance(x, basestring): 132 | return to_unicode(x) 133 | 134 | try: 135 | l = list(x) 136 | except TypeError, e: 137 | assert 'is not iterable' in str(e) 138 | return x 139 | else: 140 | return [ to_unicode(e) for e in l ] 141 | 142 | def to_utf8_optional_iterator(x): 143 | """ 144 | Raise TypeError if x is a str or if x is an iterable which 145 | contains a str. 146 | """ 147 | if isinstance(x, basestring): 148 | return to_utf8(x) 149 | 150 | try: 151 | l = list(x) 152 | except TypeError, e: 153 | assert 'is not iterable' in str(e) 154 | return x 155 | else: 156 | return [ to_utf8_if_string(e) for e in l ] 157 | 158 | def escape(s): 159 | """Escape a URL including any /.""" 160 | return urllib.quote(s.encode('utf-8'), safe='~') 161 | 162 | def generate_timestamp(): 163 | """Get seconds since epoch (UTC).""" 164 | return int(time.time()) 165 | 166 | 167 | def generate_nonce(length=8): 168 | """Generate pseudorandom number.""" 169 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 170 | 171 | 172 | def generate_verifier(length=8): 173 | """Generate pseudorandom number.""" 174 | return ''.join([str(random.randint(0, 9)) for i in range(length)]) 175 | 176 | 177 | class Consumer(object): 178 | """A consumer of OAuth-protected services. 179 | 180 | The OAuth consumer is a "third-party" service that wants to access 181 | protected resources from an OAuth service provider on behalf of an end 182 | user. It's kind of the OAuth client. 183 | 184 | Usually a consumer must be registered with the service provider by the 185 | developer of the consumer software. As part of that process, the service 186 | provider gives the consumer a *key* and a *secret* with which the consumer 187 | software can identify itself to the service. The consumer will include its 188 | key in each request to identify itself, but will use its secret only when 189 | signing requests, to prove that the request is from that particular 190 | registered consumer. 191 | 192 | Once registered, the consumer can then use its consumer credentials to ask 193 | the service provider for a request token, kicking off the OAuth 194 | authorization process. 195 | """ 196 | 197 | key = None 198 | secret = None 199 | 200 | def __init__(self, key, secret): 201 | self.key = key 202 | self.secret = secret 203 | 204 | if self.key is None or self.secret is None: 205 | raise ValueError("Key and secret must be set.") 206 | 207 | def __str__(self): 208 | data = {'oauth_consumer_key': self.key, 209 | 'oauth_consumer_secret': self.secret} 210 | 211 | return urllib.urlencode(data) 212 | 213 | 214 | class Token(object): 215 | """An OAuth credential used to request authorization or a protected 216 | resource. 217 | 218 | Tokens in OAuth comprise a *key* and a *secret*. The key is included in 219 | requests to identify the token being used, but the secret is used only in 220 | the signature, to prove that the requester is who the server gave the 221 | token to. 222 | 223 | When first negotiating the authorization, the consumer asks for a *request 224 | token* that the live user authorizes with the service provider. The 225 | consumer then exchanges the request token for an *access token* that can 226 | be used to access protected resources. 227 | """ 228 | 229 | key = None 230 | secret = None 231 | callback = None 232 | callback_confirmed = None 233 | verifier = None 234 | 235 | def __init__(self, key, secret): 236 | self.key = key 237 | self.secret = secret 238 | 239 | if self.key is None or self.secret is None: 240 | raise ValueError("Key and secret must be set.") 241 | 242 | def set_callback(self, callback): 243 | self.callback = callback 244 | self.callback_confirmed = 'true' 245 | 246 | def set_verifier(self, verifier=None): 247 | if verifier is not None: 248 | self.verifier = verifier 249 | else: 250 | self.verifier = generate_verifier() 251 | 252 | def get_callback_url(self): 253 | if self.callback and self.verifier: 254 | # Append the oauth_verifier. 255 | parts = urlparse.urlparse(self.callback) 256 | scheme, netloc, path, params, query, fragment = parts[:6] 257 | if query: 258 | query = '%s&oauth_verifier=%s' % (query, self.verifier) 259 | else: 260 | query = 'oauth_verifier=%s' % self.verifier 261 | return urlparse.urlunparse((scheme, netloc, path, params, 262 | query, fragment)) 263 | return self.callback 264 | 265 | def to_string(self): 266 | """Returns this token as a plain string, suitable for storage. 267 | 268 | The resulting string includes the token's secret, so you should never 269 | send or store this string where a third party can read it. 270 | """ 271 | 272 | data = { 273 | 'oauth_token': self.key, 274 | 'oauth_token_secret': self.secret, 275 | } 276 | 277 | if self.callback_confirmed is not None: 278 | data['oauth_callback_confirmed'] = self.callback_confirmed 279 | return urllib.urlencode(data) 280 | 281 | @staticmethod 282 | def from_string(s): 283 | """Deserializes a token from a string like one returned by 284 | `to_string()`.""" 285 | 286 | if not len(s): 287 | raise ValueError("Invalid parameter string.") 288 | 289 | params = parse_qs(s, keep_blank_values=False) 290 | if not len(params): 291 | raise ValueError("Invalid parameter string.") 292 | 293 | try: 294 | key = params['oauth_token'][0] 295 | except Exception: 296 | raise ValueError("'oauth_token' not found in OAuth request.") 297 | 298 | try: 299 | secret = params['oauth_token_secret'][0] 300 | except Exception: 301 | raise ValueError("'oauth_token_secret' not found in " 302 | "OAuth request.") 303 | 304 | token = Token(key, secret) 305 | try: 306 | token.callback_confirmed = params['oauth_callback_confirmed'][0] 307 | except KeyError: 308 | pass # 1.0, no callback confirmed. 309 | return token 310 | 311 | def __str__(self): 312 | return self.to_string() 313 | 314 | 315 | def setter(attr): 316 | name = attr.__name__ 317 | 318 | def getter(self): 319 | try: 320 | return self.__dict__[name] 321 | except KeyError: 322 | raise AttributeError(name) 323 | 324 | def deleter(self): 325 | del self.__dict__[name] 326 | 327 | return property(getter, attr, deleter) 328 | 329 | 330 | class Request(dict): 331 | 332 | """The parameters and information for an HTTP request, suitable for 333 | authorizing with OAuth credentials. 334 | 335 | When a consumer wants to access a service's protected resources, it does 336 | so using a signed HTTP request identifying itself (the consumer) with its 337 | key, and providing an access token authorized by the end user to access 338 | those resources. 339 | 340 | """ 341 | 342 | version = OAUTH_VERSION 343 | 344 | def __init__(self, method=HTTP_METHOD, url=None, parameters=None, 345 | body='', is_form_encoded=False): 346 | if url is not None: 347 | self.url = to_unicode(url) 348 | self.method = method 349 | if parameters is not None: 350 | for k, v in parameters.iteritems(): 351 | k = to_unicode(k) 352 | v = to_unicode_optional_iterator(v) 353 | self[k] = v 354 | self.body = body 355 | self.is_form_encoded = is_form_encoded 356 | 357 | 358 | @setter 359 | def url(self, value): 360 | self.__dict__['url'] = value 361 | if value is not None: 362 | scheme, netloc, path, params, query, fragment = urlparse.urlparse(value) 363 | 364 | # Exclude default port numbers. 365 | if scheme == 'http' and netloc[-3:] == ':80': 366 | netloc = netloc[:-3] 367 | elif scheme == 'https' and netloc[-4:] == ':443': 368 | netloc = netloc[:-4] 369 | if scheme not in ('http', 'https'): 370 | raise ValueError("Unsupported URL %s (%s)." % (value, scheme)) 371 | 372 | # Normalized URL excludes params, query, and fragment. 373 | self.normalized_url = urlparse.urlunparse((scheme, netloc, path, None, None, None)) 374 | else: 375 | self.normalized_url = None 376 | self.__dict__['url'] = None 377 | 378 | @setter 379 | def method(self, value): 380 | self.__dict__['method'] = value.upper() 381 | 382 | def _get_timestamp_nonce(self): 383 | return self['oauth_timestamp'], self['oauth_nonce'] 384 | 385 | def get_nonoauth_parameters(self): 386 | """Get any non-OAuth parameters.""" 387 | return dict([(k, v) for k, v in self.iteritems() 388 | if not k.startswith('oauth_')]) 389 | 390 | def to_header(self, realm=''): 391 | """Serialize as a header for an HTTPAuth request.""" 392 | oauth_params = ((k, v) for k, v in self.items() 393 | if k.startswith('oauth_')) 394 | stringy_params = ((k, escape(str(v))) for k, v in oauth_params) 395 | header_params = ('%s="%s"' % (k, v) for k, v in stringy_params) 396 | params_header = ', '.join(header_params) 397 | 398 | auth_header = 'OAuth realm="%s"' % realm 399 | if params_header: 400 | auth_header = "%s, %s" % (auth_header, params_header) 401 | 402 | return {'Authorization': auth_header} 403 | 404 | def to_postdata(self): 405 | """Serialize as post data for a POST request.""" 406 | d = {} 407 | for k, v in self.iteritems(): 408 | d[k.encode('utf-8')] = to_utf8_optional_iterator(v) 409 | 410 | # tell urlencode to deal with sequence values and map them correctly 411 | # to resulting querystring. for example self["k"] = ["v1", "v2"] will 412 | # result in 'k=v1&k=v2' and not k=%5B%27v1%27%2C+%27v2%27%5D 413 | return urllib.urlencode(d, True).replace('+', '%20') 414 | 415 | def to_url(self): 416 | """Serialize as a URL for a GET request.""" 417 | base_url = urlparse.urlparse(self.url) 418 | try: 419 | query = base_url.query 420 | except AttributeError: 421 | # must be python <2.5 422 | query = base_url[4] 423 | query = parse_qs(query) 424 | for k, v in self.items(): 425 | query.setdefault(k, []).append(v) 426 | 427 | try: 428 | scheme = base_url.scheme 429 | netloc = base_url.netloc 430 | path = base_url.path 431 | params = base_url.params 432 | fragment = base_url.fragment 433 | except AttributeError: 434 | # must be python <2.5 435 | scheme = base_url[0] 436 | netloc = base_url[1] 437 | path = base_url[2] 438 | params = base_url[3] 439 | fragment = base_url[5] 440 | 441 | url = (scheme, netloc, path, params, 442 | urllib.urlencode(query, True), fragment) 443 | return urlparse.urlunparse(url) 444 | 445 | def get_parameter(self, parameter): 446 | ret = self.get(parameter) 447 | if ret is None: 448 | raise Error('Parameter not found: %s' % parameter) 449 | 450 | return ret 451 | 452 | def get_normalized_parameters(self): 453 | """Return a string that contains the parameters that must be signed.""" 454 | items = [] 455 | for key, value in self.iteritems(): 456 | if key == 'oauth_signature': 457 | continue 458 | # 1.0a/9.1.1 states that kvp must be sorted by key, then by value, 459 | # so we unpack sequence values into multiple items for sorting. 460 | if isinstance(value, basestring): 461 | items.append((to_utf8_if_string(key), to_utf8(value))) 462 | else: 463 | try: 464 | value = list(value) 465 | except TypeError, e: 466 | assert 'is not iterable' in str(e) 467 | items.append((to_utf8_if_string(key), to_utf8_if_string(value))) 468 | else: 469 | items.extend((to_utf8_if_string(key), to_utf8_if_string(item)) for item in value) 470 | 471 | # Include any query string parameters from the provided URL 472 | query = urlparse.urlparse(self.url)[4] 473 | 474 | url_items = self._split_url_string(query).items() 475 | url_items = [(to_utf8(k), to_utf8(v)) for k, v in url_items if k != 'oauth_signature' ] 476 | items.extend(url_items) 477 | 478 | items.sort() 479 | encoded_str = urllib.urlencode(items) 480 | # Encode signature parameters per Oauth Core 1.0 protocol 481 | # spec draft 7, section 3.6 482 | # (http://tools.ietf.org/html/draft-hammer-oauth-07#section-3.6) 483 | # Spaces must be encoded with "%20" instead of "+" 484 | return encoded_str.replace('+', '%20').replace('%7E', '~') 485 | 486 | def sign_request(self, signature_method, consumer, token): 487 | """Set the signature parameter to the result of sign.""" 488 | 489 | if not self.is_form_encoded: 490 | # according to 491 | # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/oauth-bodyhash.html 492 | # section 4.1.1 "OAuth Consumers MUST NOT include an 493 | # oauth_body_hash parameter on requests with form-encoded 494 | # request bodies." 495 | self['oauth_body_hash'] = base64.b64encode(sha(self.body).digest()) 496 | 497 | if 'oauth_consumer_key' not in self: 498 | self['oauth_consumer_key'] = consumer.key 499 | 500 | if token and 'oauth_token' not in self: 501 | self['oauth_token'] = token.key 502 | 503 | self['oauth_signature_method'] = signature_method.name 504 | self['oauth_signature'] = signature_method.sign(self, consumer, token) 505 | 506 | @classmethod 507 | def make_timestamp(cls): 508 | """Get seconds since epoch (UTC).""" 509 | return str(int(time.time())) 510 | 511 | @classmethod 512 | def make_nonce(cls): 513 | """Generate pseudorandom number.""" 514 | return str(random.randint(0, 100000000)) 515 | 516 | @classmethod 517 | def from_request(cls, http_method, http_url, headers=None, parameters=None, 518 | query_string=None): 519 | """Combines multiple parameter sources.""" 520 | if parameters is None: 521 | parameters = {} 522 | 523 | # Headers 524 | if headers and 'Authorization' in headers: 525 | auth_header = headers['Authorization'] 526 | # Check that the authorization header is OAuth. 527 | if auth_header[:6] == 'OAuth ': 528 | auth_header = auth_header[6:] 529 | try: 530 | # Get the parameters from the header. 531 | header_params = cls._split_header(auth_header) 532 | parameters.update(header_params) 533 | except: 534 | raise Error('Unable to parse OAuth parameters from ' 535 | 'Authorization header.') 536 | 537 | # GET or POST query string. 538 | if query_string: 539 | query_params = cls._split_url_string(query_string) 540 | parameters.update(query_params) 541 | 542 | # URL parameters. 543 | param_str = urlparse.urlparse(http_url)[4] # query 544 | url_params = cls._split_url_string(param_str) 545 | parameters.update(url_params) 546 | 547 | if parameters: 548 | return cls(http_method, http_url, parameters) 549 | 550 | return None 551 | 552 | @classmethod 553 | def from_consumer_and_token(cls, consumer, token=None, 554 | http_method=HTTP_METHOD, http_url=None, parameters=None, 555 | body='', is_form_encoded=False): 556 | if not parameters: 557 | parameters = {} 558 | 559 | defaults = { 560 | 'oauth_consumer_key': consumer.key, 561 | 'oauth_timestamp': cls.make_timestamp(), 562 | 'oauth_nonce': cls.make_nonce(), 563 | 'oauth_version': cls.version, 564 | } 565 | 566 | defaults.update(parameters) 567 | parameters = defaults 568 | 569 | if token: 570 | parameters['oauth_token'] = token.key 571 | if token.verifier: 572 | parameters['oauth_verifier'] = token.verifier 573 | 574 | return Request(http_method, http_url, parameters, body=body, 575 | is_form_encoded=is_form_encoded) 576 | 577 | @classmethod 578 | def from_token_and_callback(cls, token, callback=None, 579 | http_method=HTTP_METHOD, http_url=None, parameters=None): 580 | 581 | if not parameters: 582 | parameters = {} 583 | 584 | parameters['oauth_token'] = token.key 585 | 586 | if callback: 587 | parameters['oauth_callback'] = callback 588 | 589 | return cls(http_method, http_url, parameters) 590 | 591 | @staticmethod 592 | def _split_header(header): 593 | """Turn Authorization: header into parameters.""" 594 | params = {} 595 | parts = header.split(',') 596 | for param in parts: 597 | # Ignore realm parameter. 598 | if param.find('realm') > -1: 599 | continue 600 | # Remove whitespace. 601 | param = param.strip() 602 | # Split key-value. 603 | param_parts = param.split('=', 1) 604 | # Remove quotes and unescape the value. 605 | params[param_parts[0]] = urllib.unquote(param_parts[1].strip('\"')) 606 | return params 607 | 608 | @staticmethod 609 | def _split_url_string(param_str): 610 | """Turn URL string into parameters.""" 611 | parameters = parse_qs(param_str.encode('utf-8'), keep_blank_values=True) 612 | for k, v in parameters.iteritems(): 613 | parameters[k] = urllib.unquote(v[0]) 614 | return parameters 615 | 616 | 617 | class Client(httplib2.Http): 618 | """OAuthClient is a worker to attempt to execute a request.""" 619 | 620 | def __init__(self, consumer, token=None, cache=None, timeout=None, 621 | proxy_info=None): 622 | 623 | if consumer is not None and not isinstance(consumer, Consumer): 624 | raise ValueError("Invalid consumer.") 625 | 626 | if token is not None and not isinstance(token, Token): 627 | raise ValueError("Invalid token.") 628 | 629 | self.consumer = consumer 630 | self.token = token 631 | self.method = SignatureMethod_HMAC_SHA1() 632 | 633 | httplib2.Http.__init__(self, cache=cache, timeout=timeout, proxy_info=proxy_info) 634 | 635 | def set_signature_method(self, method): 636 | if not isinstance(method, SignatureMethod): 637 | raise ValueError("Invalid signature method.") 638 | 639 | self.method = method 640 | 641 | def request(self, uri, method="GET", body='', headers=None, 642 | redirections=httplib2.DEFAULT_MAX_REDIRECTS, connection_type=None): 643 | DEFAULT_POST_CONTENT_TYPE = 'application/x-www-form-urlencoded' 644 | 645 | if not isinstance(headers, dict): 646 | headers = {} 647 | 648 | if method == "POST": 649 | headers['Content-Type'] = headers.get('Content-Type', 650 | DEFAULT_POST_CONTENT_TYPE) 651 | 652 | is_form_encoded = \ 653 | headers.get('Content-Type') == 'application/x-www-form-urlencoded' 654 | 655 | if is_form_encoded and body: 656 | parameters = parse_qs(body) 657 | else: 658 | parameters = None 659 | 660 | req = Request.from_consumer_and_token(self.consumer, 661 | token=self.token, http_method=method, http_url=uri, 662 | parameters=parameters, body=body, is_form_encoded=is_form_encoded) 663 | 664 | req.sign_request(self.method, self.consumer, self.token) 665 | 666 | schema, rest = urllib.splittype(uri) 667 | if rest.startswith('//'): 668 | hierpart = '//' 669 | else: 670 | hierpart = '' 671 | host, rest = urllib.splithost(rest) 672 | 673 | realm = schema + ':' + hierpart + host 674 | 675 | if is_form_encoded: 676 | body = req.to_postdata() 677 | elif method == "GET": 678 | uri = req.to_url() 679 | else: 680 | headers.update(req.to_header(realm=realm)) 681 | 682 | return httplib2.Http.request(self, uri, method=method, body=body, 683 | headers=headers, redirections=redirections, 684 | connection_type=connection_type) 685 | 686 | 687 | class Server(object): 688 | """A skeletal implementation of a service provider, providing protected 689 | resources to requests from authorized consumers. 690 | 691 | This class implements the logic to check requests for authorization. You 692 | can use it with your web server or web framework to protect certain 693 | resources with OAuth. 694 | """ 695 | 696 | timestamp_threshold = 300 # In seconds, five minutes. 697 | version = OAUTH_VERSION 698 | signature_methods = None 699 | 700 | def __init__(self, signature_methods=None): 701 | self.signature_methods = signature_methods or {} 702 | 703 | def add_signature_method(self, signature_method): 704 | self.signature_methods[signature_method.name] = signature_method 705 | return self.signature_methods 706 | 707 | def verify_request(self, request, consumer, token): 708 | """Verifies an api call and checks all the parameters.""" 709 | 710 | self._check_version(request) 711 | self._check_signature(request, consumer, token) 712 | parameters = request.get_nonoauth_parameters() 713 | return parameters 714 | 715 | def build_authenticate_header(self, realm=''): 716 | """Optional support for the authenticate header.""" 717 | return {'WWW-Authenticate': 'OAuth realm="%s"' % realm} 718 | 719 | def _check_version(self, request): 720 | """Verify the correct version of the request for this server.""" 721 | version = self._get_version(request) 722 | if version and version != self.version: 723 | raise Error('OAuth version %s not supported.' % str(version)) 724 | 725 | def _get_version(self, request): 726 | """Return the version of the request for this server.""" 727 | try: 728 | version = request.get_parameter('oauth_version') 729 | except: 730 | version = OAUTH_VERSION 731 | 732 | return version 733 | 734 | def _get_signature_method(self, request): 735 | """Figure out the signature with some defaults.""" 736 | try: 737 | signature_method = request.get_parameter('oauth_signature_method') 738 | except: 739 | signature_method = SIGNATURE_METHOD 740 | 741 | try: 742 | # Get the signature method object. 743 | signature_method = self.signature_methods[signature_method] 744 | except: 745 | signature_method_names = ', '.join(self.signature_methods.keys()) 746 | raise Error('Signature method %s not supported try one of the following: %s' % (signature_method, signature_method_names)) 747 | 748 | return signature_method 749 | 750 | def _get_verifier(self, request): 751 | return request.get_parameter('oauth_verifier') 752 | 753 | def _check_signature(self, request, consumer, token): 754 | timestamp, nonce = request._get_timestamp_nonce() 755 | self._check_timestamp(timestamp) 756 | signature_method = self._get_signature_method(request) 757 | 758 | try: 759 | signature = request.get_parameter('oauth_signature') 760 | except: 761 | raise MissingSignature('Missing oauth_signature.') 762 | 763 | # Validate the signature. 764 | valid = signature_method.check(request, consumer, token, signature) 765 | 766 | if not valid: 767 | key, base = signature_method.signing_base(request, consumer, token) 768 | 769 | raise Error('Invalid signature. Expected signature base ' 770 | 'string: %s' % base) 771 | 772 | def _check_timestamp(self, timestamp): 773 | """Verify that timestamp is recentish.""" 774 | timestamp = int(timestamp) 775 | now = int(time.time()) 776 | lapsed = now - timestamp 777 | if lapsed > self.timestamp_threshold: 778 | raise Error('Expired timestamp: given %d and now %s has a ' 779 | 'greater difference than threshold %d' % (timestamp, now, 780 | self.timestamp_threshold)) 781 | 782 | 783 | class SignatureMethod(object): 784 | """A way of signing requests. 785 | 786 | The OAuth protocol lets consumers and service providers pick a way to sign 787 | requests. This interface shows the methods expected by the other `oauth` 788 | modules for signing requests. Subclass it and implement its methods to 789 | provide a new way to sign requests. 790 | """ 791 | 792 | def signing_base(self, request, consumer, token): 793 | """Calculates the string that needs to be signed. 794 | 795 | This method returns a 2-tuple containing the starting key for the 796 | signing and the message to be signed. The latter may be used in error 797 | messages to help clients debug their software. 798 | 799 | """ 800 | raise NotImplementedError 801 | 802 | def sign(self, request, consumer, token): 803 | """Returns the signature for the given request, based on the consumer 804 | and token also provided. 805 | 806 | You should use your implementation of `signing_base()` to build the 807 | message to sign. Otherwise it may be less useful for debugging. 808 | 809 | """ 810 | raise NotImplementedError 811 | 812 | def check(self, request, consumer, token, signature): 813 | """Returns whether the given signature is the correct signature for 814 | the given consumer and token signing the given request.""" 815 | built = self.sign(request, consumer, token) 816 | return built == signature 817 | 818 | 819 | class SignatureMethod_HMAC_SHA1(SignatureMethod): 820 | name = 'HMAC-SHA1' 821 | 822 | def signing_base(self, request, consumer, token): 823 | if not hasattr(request, 'normalized_url') or request.normalized_url is None: 824 | raise ValueError("Base URL for request is not set.") 825 | 826 | sig = ( 827 | escape(request.method), 828 | escape(request.normalized_url), 829 | escape(request.get_normalized_parameters()), 830 | ) 831 | 832 | key = '%s&' % escape(consumer.secret) 833 | if token: 834 | key += escape(token.secret) 835 | raw = '&'.join(sig) 836 | return key, raw 837 | 838 | def sign(self, request, consumer, token): 839 | """Builds the base signature string.""" 840 | key, raw = self.signing_base(request, consumer, token) 841 | 842 | hashed = hmac.new(key, raw, sha) 843 | 844 | # Calculate the digest base 64. 845 | return binascii.b2a_base64(hashed.digest())[:-1] 846 | 847 | 848 | class SignatureMethod_PLAINTEXT(SignatureMethod): 849 | 850 | name = 'PLAINTEXT' 851 | 852 | def signing_base(self, request, consumer, token): 853 | """Concatenates the consumer key and secret with the token's 854 | secret.""" 855 | sig = '%s&' % escape(consumer.secret) 856 | if token: 857 | sig = sig + escape(token.secret) 858 | return sig, sig 859 | 860 | def sign(self, request, consumer, token): 861 | key, raw = self.signing_base(request, consumer, token) 862 | return raw 863 | -------------------------------------------------------------------------------- /plugin.video.tumblrv/addon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys, ssl, time, datetime, json 3 | from kodiswift import Plugin, ListItem, xbmc, xbmcgui, xbmcvfs, xbmcaddon, xbmcplugin, xbmcmixin 4 | from resources.lib import getoauth, TUMBLRAUTH, TumblrRestClient, tumblrsearch 5 | try: 6 | from xbmcutil import viewModes 7 | except: 8 | pass 9 | tclient = TumblrRestClient 10 | viewmode = 20 11 | APIOK = False 12 | plugin = Plugin(name="TumblrV", addon_id="plugin.video.tumblrv", plugin_file="addon.py", info_type="video") 13 | __addondir__ = xbmc.translatePath(plugin.addon.getAddonInfo('path')) 14 | __resdir__ = os.path.join(__addondir__, 'resources') 15 | __imgdir__ = os.path.join(__resdir__, 'images') 16 | __imgsearch__ = os.path.join(__imgdir__, 'search.png') 17 | __imgnext__ = os.path.join(__imgdir__, 'next.png') 18 | __imgtumblr__ = os.path.join(__imgdir__, 'tumblr.png') 19 | tagpath = os.path.join(xbmc.translatePath('special://profile/addon_data/'), 'plugin.video.tumblrv', 'tagslist.json') 20 | weekdelta = datetime.timedelta(days=7) 21 | 22 | 23 | @plugin.route('/') 24 | def index(): 25 | #setview_list() 26 | litems = [] 27 | itemdashvids = {} 28 | itemliked = {} 29 | itemfollowing = {} 30 | itemtagbrowse = {} 31 | itemtagged = {} 32 | itemsearch = {} 33 | tstamp = str(time.mktime((datetime.datetime.now() - weekdelta).timetuple())).split('.', 1)[0] 34 | try: 35 | itemdashvids = { 36 | 'label': 'Dashboard Videos', 37 | 'thumbnail': __imgtumblr__, 38 | 'path': plugin.url_for(endpoint=dashboard, offset=0, lastid=0), 39 | 'is_playable': False} 40 | itemliked = { 41 | 'label': 'Liked Videos', 42 | 'thumbnail': __imgtumblr__, 43 | 'path': plugin.url_for(endpoint=liked, offset=0), 44 | 'is_playable': False} 45 | itemfollowing = { 46 | 'label': 'Following', 47 | 'thumbnail': __imgtumblr__, 48 | 'path': plugin.url_for(endpoint=following, offset=0), 49 | 'is_playable': False} 50 | itemtagbrowse = { 51 | 'label': 'Browse Tags', 52 | 'thumbnail': __imgtumblr__, 53 | 'path': plugin.url_for(endpoint=taglist, timestamp=str(tstamp)), 54 | 'is_playable': False} 55 | itemtagged = { 56 | 'label': 'Search Tags', 57 | 'thumbnail': __imgtumblr__, 58 | 'path': plugin.url_for(endpoint=tags, tagname='0', timestamp=str(tstamp)), 59 | 'is_playable': False} 60 | itemsearch = { 61 | 'label': 'Search Tumblr', 62 | 'thumbnail': __imgsearch__, 63 | 'path': plugin.url_for(endpoint=search), 64 | 'is_playable': False} 65 | litems.append(itemdashvids) 66 | litems.append(itemliked) 67 | litems.append(itemfollowing) 68 | litems.append(itemtagbrowse) 69 | litems.append(itemtagged) 70 | litems.append(itemsearch) 71 | except Exception, e: 72 | plugin.notify(msg=e.message, delay=10000) 73 | if not APIOK: 74 | itemappkey = { 75 | 'label': "Consumer KEY:\n{0}".format(TUMBLRAUTH['consumer_key']), 76 | 'path': plugin.url_for(endpoint=setup)} 77 | itemappsecret = { 78 | 'label': "Consumer SECRET:\n{0}".format(TUMBLRAUTH['consumer_secret']), 79 | 'path': plugin.url_for(endpoint=setup) 80 | } 81 | itemurl = { 82 | 'label': 'https://api.tumblr.com/console/calls/user/info\nenter Key and Secret from this screen', 83 | 'path': plugin.url_for(endpoint=setup) 84 | } 85 | litems.append(itemurl) 86 | litems.append(itemappkey) 87 | litems.append(itemappsecret) 88 | return litems 89 | 90 | 91 | @plugin.route('/setup') 92 | def setup(): 93 | litems = [] 94 | itemappkey = { 95 | 'label': "Consumer KEY: {0}".format(TUMBLRAUTH['consumer_key']), 96 | 'path': plugin.keyboard(default=TUMBLRAUTH['consumer_key'], heading=TUMBLRAUTH['consumer_key'])} 97 | itemappsecret = { 98 | 'label': "Consumer SECRET: {0}".format(TUMBLRAUTH['consumer_secret']), 99 | 'path': plugin.keyboard(default=TUMBLRAUTH['consumer_secret'], heading=TUMBLRAUTH['consumer_secret']) 100 | } 101 | itemurl = { 102 | 'label': 'Visit: https://api.tumblr.com/console/calls/user/info\nenter Key and Secret from this screen', 103 | 'path': plugin.url_for(endpoint=setup) 104 | } 105 | litems.append(itemurl) 106 | litems.append(itemappkey) 107 | litems.append(itemappsecret) 108 | return litems 109 | 110 | 111 | @plugin.route('/setup/get') 112 | def setup_get(): 113 | token = plugin.keyboard(heading="OAUTH TOKEN") 114 | secret = plugin.keyboard(heading="OAUTH SECRET") 115 | plugin.set_setting('oauth_token', token) 116 | plugin.set_setting('oauth_secret', secret) 117 | TUMBLRAUTH['oauth_secret'] = secret 118 | TUMBLRAUTH['oauth_token'] = token 119 | try: 120 | client = TumblrRestClient(**TUMBLRAUTH) 121 | APIOK = True 122 | except: 123 | plugin.notify("Problem with the Tumblr OAUTH details", "Tumblr Login Failed") 124 | 125 | 126 | @plugin.route('/liked/') 127 | def liked(offset=0): 128 | #setview_thumb() 129 | likes = {} 130 | alltags = [] 131 | litems = [] 132 | listlikes = [] 133 | strpage = str(((int(offset) + 20) / 20)) 134 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage) + 1), label2="Liked Videos", icon=__imgnext__, 135 | thumbnail=__imgnext__, path=plugin.url_for(liked, offset=int(20 + int(offset)))) 136 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 137 | nextitem.is_folder = True 138 | #litems = [nextitem] 139 | results = tclient.likes(limit=20, offset=int(offset)) 140 | if results is not None: 141 | if results.get('liked_posts', '') is not None: 142 | listlikes = results.get('liked_posts', '') 143 | else: 144 | listlikes = results.get(results.keys()[-1]) 145 | for item in listlikes: 146 | if item.get('type', '') == 'video': 147 | b = {} 148 | b.update(item) 149 | lbl = "" 150 | lbl2 = "" 151 | img = __imgtumblr__ 152 | alltags.extend(item.get('tags', [])) 153 | if 'thumb' in str(item.keys()[:]): 154 | if item.get('thumbnail_url', '') is not None: 155 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 156 | elif 'image' in str(item.keys()[:]): 157 | if item.get('image_permalink', ""): 158 | img = item.get('image_permalink', "") 159 | try: 160 | plugin.log.debug(msg=item.get('thumbnail_url', '')) 161 | if len(b.get('slug', '')) > 0: 162 | lbl = b.get('slug', '') 163 | elif len(b.get('title', '')) > 0: 164 | lbl = b.get('title', '') 165 | elif len(b.get('caption', '')) > 0: 166 | lbl = Strip(b.get('caption', '')) 167 | elif len(b.get('summary', '')) > 0: 168 | lbl = b.get('summary', '') 169 | elif len(b.get('source_title', '')) > 0: 170 | lbl = b.get('source_title', '') 171 | else: 172 | lbl = b.get('short_url', '') 173 | if len(item.get('summary', '')) > 0: 174 | lbl2 = item.get('summary', '') 175 | else: 176 | lbl2 = item.get('blog_name', "") + " / " + item.get('source_title', '') + "(" + item.get( 177 | 'slug_name', '') + ")" 178 | except: 179 | lbl = b.get(b.keys()[0], "") 180 | lbl2 = b.get(b.keys()[-1], "") 181 | vidurl = item.get('video_url', "") 182 | if vidurl is not None and len(vidurl) > 10: 183 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 184 | litem.playable = True 185 | litem.is_folder = False 186 | if item.get('date', '') is not None: 187 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 188 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 189 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 190 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 191 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ]) 192 | litems.append(litem) 193 | savetags(alltags) 194 | litems.append(nextitem) 195 | return litems 196 | 197 | 198 | @plugin.route('/taglist/') 199 | def taglist(timestamp=0): 200 | #setview_list() 201 | if not os.path.exists(tagpath): 202 | json.dump([], fp=open(tagpath, mode='w')) 203 | litems = [] 204 | alltags = json.load(open(tagpath)) 205 | for tag in alltags: 206 | turl = plugin.url_for(tags, tagname=tag, timestamp=str(timestamp)) 207 | li = ListItem(label=tag, label2=tag, icon=__imgtumblr__, thumbnail=__imgtumblr__, path=turl) 208 | li.is_folder = True 209 | litems.append(li) 210 | return litems 211 | 212 | 213 | def setview_list(): 214 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 215 | str(plugin.get_setting('viewmodelist')), 216 | str(plugin.get_setting('viewmodethumb')))) 217 | try: 218 | if int(plugin.get_setting('viewmodelist')) == 0: 219 | viewselector = viewModes.Selector(20) 220 | viewmode = viewselector.currentMode 221 | plugin.set_setting('viewmodelist', viewmode) 222 | except: 223 | plugin.set_setting('viewmodelist', 20) 224 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 225 | str(plugin.get_setting('viewmodelist')), 226 | str(plugin.get_setting('viewmodethumb')))) 227 | 228 | def setview_thumb(): 229 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 230 | str(plugin.get_setting('viewmodelist')), 231 | str(plugin.get_setting('viewmodethumb')))) 232 | try: 233 | if int(plugin.get_setting('viewmodethumb')) == 0: 234 | viewselector = viewModes.Selector(500) 235 | viewmode = viewselector.currentMode 236 | plugin.set_setting('viewmodethumb', viewmode) 237 | except: 238 | plugin.set_setting('viewmodethumb', 500) 239 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 240 | str(plugin.get_setting('viewmodelist')), 241 | str(plugin.get_setting('viewmodethumb')))) 242 | 243 | 244 | @plugin.route('/tags//') 245 | def tags(tagname='', timestamp=0): 246 | atags = {} 247 | taglist = [] 248 | litems = [] 249 | if tagname == '0': 250 | tagname = plugin.keyboard(plugin.get_setting('lastsearch'), 'Search for tags') 251 | plugin.set_setting('lastsearch', tagname) 252 | nextstamp = time.mktime((datetime.datetime.fromtimestamp(float(timestamp)) - weekdelta).timetuple()) 253 | nstamp = str(nextstamp).split('.', 1)[0] 254 | nextitem = ListItem(label="Next -> {0}".format(time.ctime(nextstamp)), label2="Tagged Videos", icon=__imgnext__, 255 | thumbnail=__imgnext__, path=plugin.url_for(tags, tagname=tagname, timestamp=nstamp)) 256 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 257 | nextitem.is_folder = True 258 | #litems = [nextitem] 259 | if tagname is not None and len(tagname) > 0: 260 | results = tclient.tagged(tagname, filter='text') #), before=float(timestamp)) 261 | if results is not None: 262 | for res in results: 263 | if res.get('type', '') == 'video': taglist.append(res) 264 | for item in taglist: 265 | b = {} 266 | b.update(item) 267 | lbl = "" 268 | lbl2 = "" 269 | img = __imgtumblr__ 270 | if 'thumb' in str(item.keys()[:]): 271 | if item.get('thumbnail_url', '') is not None: 272 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 273 | elif 'image' in str(item.keys()[:]): 274 | if item.get('image_permalink', ""): 275 | img = item.get('image_permalink', "") 276 | try: 277 | plugin.log.debug(msg=item.get('thumbnail_url', '')) 278 | if len(b.get('slug', '')) > 0: 279 | lbl = b.get('slug', '') 280 | elif len(b.get('title', '')) > 0: 281 | lbl = b.get('title', '') 282 | elif len(b.get('caption', '')) > 0: 283 | lbl = Strip(b.get('caption', '')) 284 | elif len(b.get('summary', '')) > 0: 285 | lbl = b.get('summary', '') 286 | elif len(b.get('source_title', '')) > 0: 287 | lbl = b.get('source_title', '') 288 | else: 289 | lbl = b.get('short_url', '') 290 | if len(item.get('summary', '')) > 0: 291 | lbl2 = item.get('summary', '') 292 | else: 293 | lbl2 = item.get('blog_name', "") + " / " + item.get('source_title', '') + "(" + item.get( 294 | 'slug_name', '') + ")" 295 | except: 296 | lbl = b.get(b.keys()[0], "") 297 | lbl2 = b.get(b.keys()[-1], "") 298 | vidurl = item.get('video_url', "") 299 | if vidurl is not None and len(vidurl) > 10: 300 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 301 | litem.playable = True 302 | litem.is_folder = False 303 | if item.get('date', '') is not None: 304 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 305 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 306 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 307 | litems.append(litem) 308 | litems = [nextitem] 309 | return litems 310 | 311 | 312 | @plugin.route('/dashboard//') 313 | def dashboard(lastid=0, offset=0): 314 | #setview_thumb() 315 | likes = {} 316 | listlikes = [] 317 | litems = [] 318 | strpage = str(((int(offset) + 20) / 20)) 319 | # results = tclient.dashboard(offset=offset, limit=100) 320 | lastid = plugin.get_setting('lastid', int) 321 | if lastid == 0: 322 | lastid = 10000000000 323 | if lastid is None or lastid < 1000000: 324 | lastid = 10000000000 325 | results = tclient.dashboard(limit=20, offset=offset, type='video', since_id=lastid) 326 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage)+1), label2="Liked Videos", icon=__imgnext__, thumbnail=__imgnext__, path=plugin.url_for(dashboard, offset=int(20+int(offset)), lastid=lastid)) 327 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 328 | nextitem.is_folder = True 329 | # litems = [nextitem] 330 | litems = [] 331 | alltags = [] 332 | if results is not None: 333 | if results.get('posts', '') is not None: 334 | if results.get('posts', ''): 335 | results = results.get('posts', '') 336 | try: 337 | if isinstance(results, list): 338 | listlikes = results 339 | else: 340 | listlikes = results.get(results.keys()[0]) 341 | except: 342 | listlikes = [] 343 | else: 344 | listlikes = results.get(results.keys()[-1]) 345 | for item in listlikes: 346 | if item.get('type', '') == 'video': 347 | b = item 348 | img = __imgtumblr__ 349 | alltags.extend(item.get('tags', [])) 350 | if 'thumb' in str(item.keys()[:]): 351 | if item.get('thumbnail_url', '') is not None: 352 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 353 | elif 'image' in str(item.keys()[:]): 354 | if item.get('image_permalink', ""): 355 | img = item.get('image_permalink', "") 356 | try: 357 | if len(b.get('slug', '')) > 0: 358 | lbl = b.get('slug', '') 359 | elif len(b.get('title', '')) > 0: 360 | lbl = b.get('title', '') 361 | elif len(b.get('caption', '')) > 0: 362 | lbl = Strip(b.get('caption', '')) 363 | elif len(b.get('summary', '')) > 0: 364 | lbl = b.get('summary', '') 365 | elif len(b.get('source_title', '')) > 0: 366 | lbl = b.get('source_title', '') 367 | else: 368 | lbl = b.get('short_url', '') 369 | if len(item.get('summary', '')) > 0: 370 | lbl2 = item.get('summary', '') 371 | else: 372 | lbl2 = item.get('blog_name', '') + " / " + item.get('source_title', '') + "(" + item.get( 373 | 'slug_name', '') + ")" 374 | except: 375 | lbl = b.get('blog_name', '') 376 | lbl2 = b.get('short_url', '') 377 | img = item.get('thumbnail_url', '') 378 | vidurl = item.get('video_url', '') 379 | if vidurl is not None and len(vidurl) > 10: 380 | if len(b.get('caption', '')) > 0: 381 | lbl = Strip(b.get('caption', '')) 382 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 383 | litem.playable = True 384 | litem.is_folder = False 385 | if item.get('date', '') is not None: 386 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 387 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 388 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 389 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 390 | pathaddlike = plugin.url_for(endpoint=addlike, id=item.get('id', '')) 391 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)),]) 392 | litems.append(litem) 393 | item = listlikes[-1] 394 | plugin.set_setting('lastid', str(item.get('id', lastid))) 395 | savetags(alltags) 396 | litems.append(nextitem) 397 | return litems 398 | 399 | 400 | @plugin.route('/addlike/') 401 | def addlike(id=0): 402 | try: 403 | tclient.like(None, id) 404 | plugin.notify(msg="LIKED: {0}".format(str(id))) 405 | except: 406 | plugin.notify(msg="Failed to add like: {0}".format(str(id))) 407 | 408 | 409 | @plugin.route('/download/') 410 | def download(urlvideo): 411 | try: 412 | from YDStreamExtractor import getVideoInfo 413 | from YDStreamExtractor import handleDownload 414 | info = getVideoInfo(urlvideo, resolve_redirects=True) 415 | dlpath = plugin.get_setting('downloadpath') 416 | if not os.path.exists(dlpath): 417 | dlpath = xbmc.translatePath("home://") 418 | handleDownload(info, bg=True, path=dlpath) 419 | except: 420 | plugin.notify(urlvideo, "Download Failed") 421 | 422 | 423 | def following_list(offset=0, max=0): 424 | litems = [] 425 | offset = 0 426 | total = 0 427 | resp = tclient.following(offset=offset, limit=20) # tclient.dashboard(type='videos') 428 | if max == 0: 429 | total = int(resp.get('total_blogs', 0)) 430 | if total > 150: total = 150 431 | else: 432 | total = 20 + max 433 | results = resp.get('response', {}) 434 | if results is not None: 435 | blogres = results.get('blogs', []) 436 | for blog in blogres: 437 | blog['updated'] = datetime.datetime.fromtimestamp(blog.get('updated'), 0).isoformat() 438 | litems.append(blog) 439 | try: 440 | for offnum in range(len(blogres), total, 20): 441 | newblogs = [] 442 | newres = tclient.following(offset=offnum, limit=20) 443 | newblogs = newres.get('blogs', []) 444 | if len(newblogs) > 0: 445 | for blog in newblogs: 446 | blog['updated'] = datetime.datetime.fromtimestamp(blog['updated']).isoformat() 447 | litems.append(blog) 448 | except: 449 | pass 450 | items = sorted(litems, key=lambda litems: litems['updated']) 451 | #plugin.notify(msg="Following: {0} Total: {1} Len: {2}".format(str(len(resp.get('blogs',[]))), str(total), str(len(litems)))) 452 | return items 453 | 454 | 455 | @plugin.route('/following/') 456 | def following(offset=0): 457 | blogs = {} 458 | litems = [] 459 | blogres = [] 460 | listblogs = [] 461 | litems = [] 462 | name = '' 463 | updated = '' 464 | url = '' 465 | desc = '' 466 | strpage = str(((int(offset) + 50) / 50)) 467 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage) + 1), label2="More", icon=__imgnext__, 468 | thumbnail=__imgnext__, path=plugin.url_for(following, offset=int(50 + int(offset)))) 469 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 470 | nextitem.is_folder = True 471 | #litems = [nextitem] 472 | results = following_list(offset=offset) # max not working right now, max=50) 473 | for b in results: 474 | thumb = __imgtumblr__ 475 | try: 476 | thumbd = {} 477 | name = b.get('name', '') 478 | title = b.get('title', '') 479 | desc = b.get('description', '') 480 | url = b.get('url', "http://{0}.tumblr.com".format(name)) 481 | updated = b.get('updated', '') 482 | thumbd = tclient.avatar(name, 128) 483 | if len(thumbd.keys()) > 0: 484 | thumb = thumbd[thumbd.keys()[0]] 485 | if len(thumb) < 1: 486 | if len(b.get('theme', '{}')) > 0: 487 | theme = b.get('theme', '{}') 488 | if len(theme.get('header_image_scaled', '')) > 0: thumb = theme.get('header_image_scaled', '') 489 | iurl = plugin.url_for(endpoint=blogposts, blogname=name, offset=0) 490 | lbl = "{0}\n{1}".format(name, title.encode('latin-1', 'ignore')) 491 | lbl2 = desc.encode('latin-1', 'ignore') 492 | litem = ListItem(label=lbl, label2=lbl2, icon=thumb, thumbnail=thumb, path=iurl) 493 | litem.set_art({'poster': thumb, 'thumbnail': thumb, 'fanart': thumb}) 494 | litem.is_folder = True 495 | litem.playable = False 496 | litems.append(litem) 497 | except: 498 | pass 499 | #items = sorted(litems, key=lambda litems: litems.label2) 500 | # litems.append(nextitem) NO NEXT PAGE TODO: Make max work and paginate results as this is slow 501 | return litems 502 | 503 | 504 | @plugin.route('/blogposts//') 505 | def blogposts(blogname, offset=0): 506 | listposts = [] 507 | lbl = '' 508 | lbl2 = '' 509 | vidurl = '' 510 | results = [] 511 | alltags = [] 512 | litems = [] 513 | if blogname.find('.') != -1: 514 | shortname = blogname.split('.', 1)[-1] 515 | if shortname.find('.') != -1: 516 | blogname = shortname.lsplit('.')[0] 517 | strpage = str((20 + int(offset)) / 20) 518 | nextitem = ListItem(label="Next Page -> #{0}".format(strpage), label2=blogname, icon=__imgnext__, 519 | thumbnail=__imgnext__, 520 | path=plugin.url_for(blogposts, blogname=blogname, offset=int(20 + int(offset)))) 521 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 522 | nextitem.is_folder = True 523 | #litems = [nextitem] 524 | results = tclient.posts(blogname=blogname, limit=20, offset=int(offset), type='video') 525 | if results is not None: 526 | if len(results.get('posts', '')) > 1: 527 | results = results.get('posts', '') 528 | for post in results: 529 | lbl2 = post.get('blog_name', '') 530 | lbl = post.get('slug', '').replace('-', ' ') 531 | img = post.get('thumbnail_url', __imgtumblr__) 532 | alltags.extend(post.get('tags', [])) 533 | try: 534 | if post.get('slug', '') is not None: 535 | lbl = post.get('slug', '').replace('-', ' ') 536 | if len(post.get('caption', '')) > 0: 537 | lbl = Strip(post.get('caption', '')) 538 | elif len(post.get('summary', '')) > 0: 539 | lbl = post.get('summary', '') 540 | elif len(post.get('source_title', '')) > 0: 541 | lbl = post.get('source_title', '') 542 | else: 543 | lbl = post.get('short_url', '') 544 | if post.get('thumbnail_url', ''): 545 | img = post.get('thumbnail_url', '') 546 | if post.get('video_url', '') is not None: 547 | vidurl = post.get('video_url', '') 548 | except: 549 | plugin.notify(str(repr(post))) 550 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 551 | litem.playable = True 552 | litem.is_folder = False 553 | if len(post.get('date', '')) > 0: 554 | rdate = str(post.get('date', '')).split(' ', 1)[0].strip() 555 | litem.set_info(info_type='video', info_labels={'Date': rdate, 'Duration': post.get('duration', '')}) 556 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 557 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 558 | pathaddlike = plugin.url_for(endpoint=addlike, id=post.get('id','')) 559 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)), ]) 560 | litems.append(litem) 561 | else: 562 | litems = [] 563 | backurl = '' 564 | if offset == 0: 565 | backurl = plugin.url_for(endpoint=following, offset=0) 566 | else: 567 | backurl = plugin.url_for(blogposts, blogname=blogname, offset=(int(offset) - 20)) 568 | nextitem = ListItem(label="No Results - GO BACK".format(strpage), label2=blogname, icon=__imgtumblr__, 569 | thumbnail=__imgtumblr__, path=backurl) 570 | nextitem.set_art({'poster': __imgtumblr__, 'thumbnail': __imgtumblr__, 'fanart': __imgtumblr__}) 571 | nextitem.is_folder = True 572 | litems = [nextitem] 573 | savetags(alltags) 574 | litems.append(nextitem) 575 | return litems 576 | 577 | 578 | @plugin.route('/search') 579 | def search(): 580 | # plugin.log.debug(TUMBLRAUTH) 581 | # client = TumblrRestClient(**TUMBLRAUTH) 582 | # info = client.info() 583 | litems = [] 584 | searchtxt = '' 585 | searchquery = '' 586 | offsetnum = 0 587 | searchtxt = plugin.get_setting('lastsearch') 588 | searchtxt = plugin.keyboard(searchtxt, 'Search All Sites', False) 589 | searchquery = searchtxt.replace(' ', '+') 590 | plugin.set_setting(key='lastsearch', val=searchtxt) 591 | results = following_list(offset=offsetnum) 592 | listmatch = [] 593 | max = 20 594 | #if len(results) < 20: 595 | # max = len(results) - 1 596 | for blog in results: 597 | name = blog.get('name', '') 598 | posts = tclient.posts(name, type='video') 599 | for post in posts.get('posts', []): 600 | for k,v in post.items(): 601 | try: 602 | if searchquery.lower() in str(v.encode('latin-1', 'ignore')).lower(): 603 | listmatch.append(post) 604 | break 605 | except: 606 | pass 607 | plugin.notify(msg="Matches: {0}".format(str(len(listmatch)))) 608 | alltags = [] 609 | for post in listmatch: 610 | lbl2 = post.get('blog_name', '') 611 | lbl = post.get('slug', '').replace('-', ' ') 612 | img = post.get('thumbnail_url', __imgtumblr__) 613 | alltags.extend(post.get('tags', [])) 614 | try: 615 | if post.get('slug', '') is not None: 616 | lbl = post.get('slug', '').replace('-', ' ') 617 | if len(post.get('caption', '')) > 0: 618 | lbl = Strip(post.get('caption', '')) 619 | elif len(post.get('summary', '')) > 0: 620 | lbl = post.get('summary', '') 621 | elif len(post.get('source_title', '')) > 0: 622 | lbl = post.get('source_title', '') 623 | else: 624 | lbl = post.get('short_url', '') 625 | if post.get('thumbnail_url', ''): 626 | img = post.get('thumbnail_url', '') 627 | if post.get('video_url', '') is not None: 628 | vidurl = post.get('video_url', '') 629 | except: 630 | plugin.notify(str(repr(post))) 631 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 632 | litem.playable = True 633 | litem.is_folder = False 634 | if len(post.get('date', '')) > 0: 635 | rdate = str(post.get('date', '')).split(' ', 1)[0].strip() 636 | litem.set_info(info_type='video', info_labels={'Date': rdate, 'Duration': post.get('duration', '')}) 637 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 638 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 639 | pathaddlike = plugin.url_for(endpoint=addlike, id=post.get('id', '')) 640 | litem.add_context_menu_items( 641 | [('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)), ]) 642 | litems.append(litem) 643 | savetags(alltags) 644 | return litems 645 | 646 | 647 | def savetags(taglist=[]): 648 | if not os.path.exists(tagpath): 649 | json.dump([], fp=open(tagpath, mode='w')) 650 | taglist.extend(json.load(open(tagpath, mode='r'))) 651 | alltags = sorted(set(taglist)) 652 | json.dump(alltags, fp=open(tagpath, mode='w')) 653 | 654 | 655 | def Strip(text): 656 | import re 657 | notagre = re.compile(r'<.+?>') 658 | return notagre.sub(' ', text).strip() 659 | 660 | 661 | if __name__ == '__main__': 662 | try: 663 | otoken = plugin.get_setting('oauth_token') 664 | osecret = plugin.get_setting('oauth_secret') 665 | TUMBLRAUTH.update({'oauth_token': otoken, 'oauth_secret': osecret}) 666 | tclient = TumblrRestClient(**TUMBLRAUTH) 667 | info = tclient.info() 668 | if info is not None and 'user' in info.keys(): 669 | APIOK = True 670 | else: 671 | APIOK = False 672 | except: 673 | APIOK = False 674 | try: 675 | TUMBLRAUTH = getoauth() 676 | tclient = TumblrRestClient(**TUMBLRAUTH) 677 | info = tclient.info() 678 | if info is not None and info.get('user', None) is not None: 679 | APIOK = True 680 | else: 681 | APIOK = False 682 | except: 683 | plugin.notify( 684 | msg="Required Tumblr OAUTH token missing..Backup plan!", 685 | title="Tumblr Login Failed", delay=10000) 686 | plugin.log.error(msg="Tumblr API OAuth settings invalid. This addon requires you to authorize this Addon in your Tumblr account and in turn in the settings you must provide the TOKEN and SECRET that Tumblr returns.\nhttps://api.tumblr.com/console/calls/user/info\n\tUse the Consumer Key and Secret from the addon settings to authorize this addon and the OAUTH Token and Secret the website returns must be put into the settings.") 687 | try: # Try an old style API key from off github as a backup so some functionality is provided? 688 | TUMBLRAUTH = dict(consumer_key='5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4', 689 | consumer_secret='GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch', 690 | oauth_token='RBesLWIhoxC1StezFBQ5EZf7A9EkdHvvuQQWyLpyy8vdj8aqvU', 691 | oauth_secret='GQAEtLIJuPojQ8fojZrh0CFBzUbqQu8cFH5ejnChQBl4ljJB4a') 692 | TUMBLRAUTH.update({'api_key', 'fuiKNFp9vQFvjLNvx4sUwti4Yb5yGutBN4Xh10LXZhhRKjWlV4'}) 693 | tclient = TumblrRestClient(**TUMBLRAUTH) 694 | except: 695 | plugin.notify(msg="Read Settings for instructions", title="COULDN'T AUTH TO TUMBLR") 696 | viewmode = int(plugin.get_setting('viewmode')) 697 | plugin.run() 698 | plugin.set_content(content='movies') 699 | viewmodel = 51 700 | viewmodet = 500 701 | if str(plugin.request.path).startswith('/taglist/') or plugin.request.path == '/': 702 | viewmodel = int(plugin.get_setting('viewmodelist')) 703 | if viewmodel == 0: viewmodel = 51 704 | plugin.set_view_mode(viewmodel) 705 | else: 706 | viewmodet = int(plugin.get_setting('viewmodethumb')) 707 | if viewmodet == 0: viewmodet = 500 708 | plugin.set_view_mode(viewmodet) 709 | -------------------------------------------------------------------------------- /zips/plugin.video.tumblrv/addon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os, sys, ssl, time, datetime, json 3 | from kodiswift import Plugin, ListItem, xbmc, xbmcgui, xbmcvfs, xbmcaddon, xbmcplugin, xbmcmixin 4 | from resources.lib import getoauth, TUMBLRAUTH, TumblrRestClient, tumblrsearch 5 | try: 6 | from xbmcutil import viewModes 7 | except: 8 | pass 9 | tclient = TumblrRestClient 10 | viewmode = 20 11 | APIOK = False 12 | plugin = Plugin(name="TumblrV", addon_id="plugin.video.tumblrv", plugin_file="addon.py", info_type="video") 13 | __addondir__ = xbmc.translatePath(plugin.addon.getAddonInfo('path')) 14 | __resdir__ = os.path.join(__addondir__, 'resources') 15 | __imgdir__ = os.path.join(__resdir__, 'images') 16 | __imgsearch__ = os.path.join(__imgdir__, 'search.png') 17 | __imgnext__ = os.path.join(__imgdir__, 'next.png') 18 | __imgtumblr__ = os.path.join(__imgdir__, 'tumblr.png') 19 | tagpath = os.path.join(xbmc.translatePath('special://profile/addon_data/'), 'plugin.video.tumblrv', 'tagslist.json') 20 | weekdelta = datetime.timedelta(days=7) 21 | 22 | 23 | @plugin.route('/') 24 | def index(): 25 | #setview_list() 26 | litems = [] 27 | itemdashvids = {} 28 | itemliked = {} 29 | itemfollowing = {} 30 | itemtagbrowse = {} 31 | itemtagged = {} 32 | itemsearch = {} 33 | tstamp = str(time.mktime((datetime.datetime.now() - weekdelta).timetuple())).split('.', 1)[0] 34 | try: 35 | itemdashvids = { 36 | 'label': 'Dashboard Videos', 37 | 'thumbnail': __imgtumblr__, 38 | 'path': plugin.url_for(endpoint=dashboard, offset=0, lastid=0), 39 | 'is_playable': False} 40 | itemliked = { 41 | 'label': 'Liked Videos', 42 | 'thumbnail': __imgtumblr__, 43 | 'path': plugin.url_for(endpoint=liked, offset=0), 44 | 'is_playable': False} 45 | itemfollowing = { 46 | 'label': 'Following', 47 | 'thumbnail': __imgtumblr__, 48 | 'path': plugin.url_for(endpoint=following, offset=0), 49 | 'is_playable': False} 50 | itemtagbrowse = { 51 | 'label': 'Browse Tags', 52 | 'thumbnail': __imgtumblr__, 53 | 'path': plugin.url_for(endpoint=taglist, timestamp=str(tstamp)), 54 | 'is_playable': False} 55 | itemtagged = { 56 | 'label': 'Search Tags', 57 | 'thumbnail': __imgtumblr__, 58 | 'path': plugin.url_for(endpoint=tags, tagname='0', timestamp=str(tstamp)), 59 | 'is_playable': False} 60 | itemsearch = { 61 | 'label': 'Search Tumblr', 62 | 'thumbnail': __imgsearch__, 63 | 'path': plugin.url_for(endpoint=search), 64 | 'is_playable': False} 65 | litems.append(itemdashvids) 66 | litems.append(itemliked) 67 | litems.append(itemfollowing) 68 | litems.append(itemtagbrowse) 69 | litems.append(itemtagged) 70 | litems.append(itemsearch) 71 | except Exception, e: 72 | plugin.notify(msg=e.message, delay=10000) 73 | if not APIOK: 74 | itemappkey = { 75 | 'label': "Consumer KEY:\n{0}".format(TUMBLRAUTH['consumer_key']), 76 | 'path': plugin.url_for(endpoint=setup)} 77 | itemappsecret = { 78 | 'label': "Consumer SECRET:\n{0}".format(TUMBLRAUTH['consumer_secret']), 79 | 'path': plugin.url_for(endpoint=setup) 80 | } 81 | itemurl = { 82 | 'label': 'https://api.tumblr.com/console/calls/user/info\nenter Key and Secret from this screen', 83 | 'path': plugin.url_for(endpoint=setup) 84 | } 85 | litems.append(itemurl) 86 | litems.append(itemappkey) 87 | litems.append(itemappsecret) 88 | return litems 89 | 90 | 91 | @plugin.route('/setup') 92 | def setup(): 93 | litems = [] 94 | itemappkey = { 95 | 'label': "Consumer KEY: {0}".format(TUMBLRAUTH['consumer_key']), 96 | 'path': plugin.keyboard(default=TUMBLRAUTH['consumer_key'], heading=TUMBLRAUTH['consumer_key'])} 97 | itemappsecret = { 98 | 'label': "Consumer SECRET: {0}".format(TUMBLRAUTH['consumer_secret']), 99 | 'path': plugin.keyboard(default=TUMBLRAUTH['consumer_secret'], heading=TUMBLRAUTH['consumer_secret']) 100 | } 101 | itemurl = { 102 | 'label': 'Visit: https://api.tumblr.com/console/calls/user/info\nenter Key and Secret from this screen', 103 | 'path': plugin.url_for(endpoint=setup) 104 | } 105 | litems.append(itemurl) 106 | litems.append(itemappkey) 107 | litems.append(itemappsecret) 108 | return litems 109 | 110 | 111 | @plugin.route('/setup/get') 112 | def setup_get(): 113 | token = plugin.keyboard(heading="OAUTH TOKEN") 114 | secret = plugin.keyboard(heading="OAUTH SECRET") 115 | plugin.set_setting('oauth_token', token) 116 | plugin.set_setting('oauth_secret', secret) 117 | TUMBLRAUTH['oauth_secret'] = secret 118 | TUMBLRAUTH['oauth_token'] = token 119 | try: 120 | client = TumblrRestClient(**TUMBLRAUTH) 121 | APIOK = True 122 | except: 123 | plugin.notify("Problem with the Tumblr OAUTH details", "Tumblr Login Failed") 124 | 125 | 126 | @plugin.route('/liked/') 127 | def liked(offset=0): 128 | #setview_thumb() 129 | likes = {} 130 | alltags = [] 131 | litems = [] 132 | listlikes = [] 133 | strpage = str(((int(offset) + 20) / 20)) 134 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage) + 1), label2="Liked Videos", icon=__imgnext__, 135 | thumbnail=__imgnext__, path=plugin.url_for(liked, offset=int(20 + int(offset)))) 136 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 137 | nextitem.is_folder = True 138 | #litems = [nextitem] 139 | results = tclient.likes(limit=20, offset=int(offset)) 140 | if results is not None: 141 | if results.get('liked_posts', '') is not None: 142 | listlikes = results.get('liked_posts', '') 143 | else: 144 | listlikes = results.get(results.keys()[-1]) 145 | for item in listlikes: 146 | if item.get('type', '') == 'video': 147 | b = {} 148 | b.update(item) 149 | lbl = "" 150 | lbl2 = "" 151 | img = __imgtumblr__ 152 | alltags.extend(item.get('tags', [])) 153 | if 'thumb' in str(item.keys()[:]): 154 | if item.get('thumbnail_url', '') is not None: 155 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 156 | elif 'image' in str(item.keys()[:]): 157 | if item.get('image_permalink', ""): 158 | img = item.get('image_permalink', "") 159 | try: 160 | plugin.log.debug(msg=item.get('thumbnail_url', '')) 161 | if len(b.get('slug', '')) > 0: 162 | lbl = b.get('slug', '') 163 | elif len(b.get('title', '')) > 0: 164 | lbl = b.get('title', '') 165 | elif len(b.get('caption', '')) > 0: 166 | lbl = Strip(b.get('caption', '')) 167 | elif len(b.get('summary', '')) > 0: 168 | lbl = b.get('summary', '') 169 | elif len(b.get('source_title', '')) > 0: 170 | lbl = b.get('source_title', '') 171 | else: 172 | lbl = b.get('short_url', '') 173 | if len(item.get('summary', '')) > 0: 174 | lbl2 = item.get('summary', '') 175 | else: 176 | lbl2 = item.get('blog_name', "") + " / " + item.get('source_title', '') + "(" + item.get( 177 | 'slug_name', '') + ")" 178 | except: 179 | lbl = b.get(b.keys()[0], "") 180 | lbl2 = b.get(b.keys()[-1], "") 181 | vidurl = item.get('video_url', "") 182 | if vidurl is not None and len(vidurl) > 10: 183 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 184 | litem.playable = True 185 | litem.is_folder = False 186 | if item.get('date', '') is not None: 187 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 188 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 189 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 190 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 191 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ]) 192 | litems.append(litem) 193 | savetags(alltags) 194 | litems.append(nextitem) 195 | return litems 196 | 197 | 198 | @plugin.route('/taglist/') 199 | def taglist(timestamp=0): 200 | #setview_list() 201 | if not os.path.exists(tagpath): 202 | json.dump([], fp=open(tagpath, mode='w')) 203 | litems = [] 204 | alltags = json.load(open(tagpath)) 205 | for tag in alltags: 206 | turl = plugin.url_for(tags, tagname=tag, timestamp=str(timestamp)) 207 | li = ListItem(label=tag, label2=tag, icon=__imgtumblr__, thumbnail=__imgtumblr__, path=turl) 208 | li.is_folder = True 209 | litems.append(li) 210 | return litems 211 | 212 | 213 | def setview_list(): 214 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 215 | str(plugin.get_setting('viewmodelist')), 216 | str(plugin.get_setting('viewmodethumb')))) 217 | try: 218 | if int(plugin.get_setting('viewmodelist')) == 0: 219 | viewselector = viewModes.Selector(20) 220 | viewmode = viewselector.currentMode 221 | plugin.set_setting('viewmodelist', viewmode) 222 | except: 223 | plugin.set_setting('viewmodelist', 20) 224 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 225 | str(plugin.get_setting('viewmodelist')), 226 | str(plugin.get_setting('viewmodethumb')))) 227 | 228 | def setview_thumb(): 229 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 230 | str(plugin.get_setting('viewmodelist')), 231 | str(plugin.get_setting('viewmodethumb')))) 232 | try: 233 | if int(plugin.get_setting('viewmodethumb')) == 0: 234 | viewselector = viewModes.Selector(500) 235 | viewmode = viewselector.currentMode 236 | plugin.set_setting('viewmodethumb', viewmode) 237 | except: 238 | plugin.set_setting('viewmodethumb', 500) 239 | plugin.notify(msg="{0} View: {1} / L{2} / T{3}".format(str(plugin.request.path), str(plugin.get_setting('viewmode')), 240 | str(plugin.get_setting('viewmodelist')), 241 | str(plugin.get_setting('viewmodethumb')))) 242 | 243 | 244 | @plugin.route('/tags//') 245 | def tags(tagname='', timestamp=0): 246 | atags = {} 247 | taglist = [] 248 | litems = [] 249 | if tagname == '0': 250 | tagname = plugin.keyboard(plugin.get_setting('lastsearch'), 'Search for tags') 251 | plugin.set_setting('lastsearch', tagname) 252 | nextstamp = time.mktime((datetime.datetime.fromtimestamp(float(timestamp)) - weekdelta).timetuple()) 253 | nstamp = str(nextstamp).split('.', 1)[0] 254 | nextitem = ListItem(label="Next -> {0}".format(time.ctime(nextstamp)), label2="Tagged Videos", icon=__imgnext__, 255 | thumbnail=__imgnext__, path=plugin.url_for(tags, tagname=tagname, timestamp=nstamp)) 256 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 257 | nextitem.is_folder = True 258 | #litems = [nextitem] 259 | if tagname is not None and len(tagname) > 0: 260 | results = tclient.tagged(tagname, filter='text') #), before=float(timestamp)) 261 | if results is not None: 262 | for res in results: 263 | if res.get('type', '') == 'video': taglist.append(res) 264 | for item in taglist: 265 | b = {} 266 | b.update(item) 267 | lbl = "" 268 | lbl2 = "" 269 | img = __imgtumblr__ 270 | if 'thumb' in str(item.keys()[:]): 271 | if item.get('thumbnail_url', '') is not None: 272 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 273 | elif 'image' in str(item.keys()[:]): 274 | if item.get('image_permalink', ""): 275 | img = item.get('image_permalink', "") 276 | try: 277 | plugin.log.debug(msg=item.get('thumbnail_url', '')) 278 | if len(b.get('slug', '')) > 0: 279 | lbl = b.get('slug', '') 280 | elif len(b.get('title', '')) > 0: 281 | lbl = b.get('title', '') 282 | elif len(b.get('caption', '')) > 0: 283 | lbl = Strip(b.get('caption', '')) 284 | elif len(b.get('summary', '')) > 0: 285 | lbl = b.get('summary', '') 286 | elif len(b.get('source_title', '')) > 0: 287 | lbl = b.get('source_title', '') 288 | else: 289 | lbl = b.get('short_url', '') 290 | if len(item.get('summary', '')) > 0: 291 | lbl2 = item.get('summary', '') 292 | else: 293 | lbl2 = item.get('blog_name', "") + " / " + item.get('source_title', '') + "(" + item.get( 294 | 'slug_name', '') + ")" 295 | except: 296 | lbl = b.get(b.keys()[0], "") 297 | lbl2 = b.get(b.keys()[-1], "") 298 | vidurl = item.get('video_url', "") 299 | if vidurl is not None and len(vidurl) > 10: 300 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 301 | litem.playable = True 302 | litem.is_folder = False 303 | if item.get('date', '') is not None: 304 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 305 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 306 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 307 | litems.append(litem) 308 | litems = [nextitem] 309 | return litems 310 | 311 | 312 | @plugin.route('/dashboard//') 313 | def dashboard(lastid=0, offset=0): 314 | #setview_thumb() 315 | likes = {} 316 | listlikes = [] 317 | litems = [] 318 | strpage = str(((int(offset) + 20) / 20)) 319 | # results = tclient.dashboard(offset=offset, limit=100) 320 | lastid = plugin.get_setting('lastid', int) 321 | if lastid == 0: 322 | lastid = 10000000000 323 | if lastid is None or lastid < 1000000: 324 | lastid = 10000000000 325 | results = tclient.dashboard(limit=20, offset=offset, type='video', since_id=lastid) 326 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage)+1), label2="Liked Videos", icon=__imgnext__, thumbnail=__imgnext__, path=plugin.url_for(dashboard, offset=int(20+int(offset)), lastid=lastid)) 327 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 328 | nextitem.is_folder = True 329 | # litems = [nextitem] 330 | litems = [] 331 | alltags = [] 332 | if results is not None: 333 | if results.get('posts', '') is not None: 334 | if results.get('posts', ''): 335 | results = results.get('posts', '') 336 | try: 337 | if isinstance(results, list): 338 | listlikes = results 339 | else: 340 | listlikes = results.get(results.keys()[0]) 341 | except: 342 | listlikes = [] 343 | else: 344 | listlikes = results.get(results.keys()[-1]) 345 | for item in listlikes: 346 | if item.get('type', '') == 'video': 347 | b = item 348 | img = __imgtumblr__ 349 | alltags.extend(item.get('tags', [])) 350 | if 'thumb' in str(item.keys()[:]): 351 | if item.get('thumbnail_url', '') is not None: 352 | img = item.get('thumbnail_url', '') # .replace('https', 'http') #item.get('thumbnail_url','') 353 | elif 'image' in str(item.keys()[:]): 354 | if item.get('image_permalink', ""): 355 | img = item.get('image_permalink', "") 356 | try: 357 | if len(b.get('slug', '')) > 0: 358 | lbl = b.get('slug', '') 359 | elif len(b.get('title', '')) > 0: 360 | lbl = b.get('title', '') 361 | elif len(b.get('caption', '')) > 0: 362 | lbl = Strip(b.get('caption', '')) 363 | elif len(b.get('summary', '')) > 0: 364 | lbl = b.get('summary', '') 365 | elif len(b.get('source_title', '')) > 0: 366 | lbl = b.get('source_title', '') 367 | else: 368 | lbl = b.get('short_url', '') 369 | if len(item.get('summary', '')) > 0: 370 | lbl2 = item.get('summary', '') 371 | else: 372 | lbl2 = item.get('blog_name', '') + " / " + item.get('source_title', '') + "(" + item.get( 373 | 'slug_name', '') + ")" 374 | except: 375 | lbl = b.get('blog_name', '') 376 | lbl2 = b.get('short_url', '') 377 | img = item.get('thumbnail_url', '') 378 | vidurl = item.get('video_url', '') 379 | if vidurl is not None and len(vidurl) > 10: 380 | if len(b.get('caption', '')) > 0: 381 | lbl = Strip(b.get('caption', '')) 382 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 383 | litem.playable = True 384 | litem.is_folder = False 385 | if item.get('date', '') is not None: 386 | rdate = str(item.get('date', '')).split(' ', 1)[0].strip() 387 | litem.set_info(info_type='video', info_labels={'Date': rdate}) 388 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 389 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 390 | pathaddlike = plugin.url_for(endpoint=addlike, id=item.get('id', '')) 391 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)),]) 392 | litems.append(litem) 393 | item = listlikes[-1] 394 | plugin.set_setting('lastid', str(item.get('id', lastid))) 395 | savetags(alltags) 396 | litems.append(nextitem) 397 | return litems 398 | 399 | 400 | @plugin.route('/addlike/') 401 | def addlike(id=0): 402 | try: 403 | tclient.like(None, id) 404 | plugin.notify(msg="LIKED: {0}".format(str(id))) 405 | except: 406 | plugin.notify(msg="Failed to add like: {0}".format(str(id))) 407 | 408 | 409 | @plugin.route('/download/') 410 | def download(urlvideo): 411 | try: 412 | from YDStreamExtractor import getVideoInfo 413 | from YDStreamExtractor import handleDownload 414 | info = getVideoInfo(urlvideo, resolve_redirects=True) 415 | dlpath = plugin.get_setting('downloadpath') 416 | if not os.path.exists(dlpath): 417 | dlpath = xbmc.translatePath("home://") 418 | handleDownload(info, bg=True, path=dlpath) 419 | except: 420 | plugin.notify(urlvideo, "Download Failed") 421 | 422 | 423 | def following_list(offset=0, max=0): 424 | litems = [] 425 | offset = 0 426 | total = 0 427 | resp = tclient.following(offset=offset, limit=20) # tclient.dashboard(type='videos') 428 | if max == 0: 429 | total = int(resp.get('total_blogs', 0)) 430 | if total > 150: total = 150 431 | else: 432 | total = 20 + max 433 | results = resp.get('response', {}) 434 | if results is not None: 435 | blogres = results.get('blogs', []) 436 | for blog in blogres: 437 | blog['updated'] = datetime.datetime.fromtimestamp(blog.get('updated'), 0).isoformat() 438 | litems.append(blog) 439 | try: 440 | for offnum in range(len(blogres), total, 20): 441 | newblogs = [] 442 | newres = tclient.following(offset=offnum, limit=20) 443 | newblogs = newres.get('blogs', []) 444 | if len(newblogs) > 0: 445 | for blog in newblogs: 446 | blog['updated'] = datetime.datetime.fromtimestamp(blog['updated']).isoformat() 447 | litems.append(blog) 448 | except: 449 | pass 450 | items = sorted(litems, key=lambda litems: litems['updated']) 451 | #plugin.notify(msg="Following: {0} Total: {1} Len: {2}".format(str(len(resp.get('blogs',[]))), str(total), str(len(litems)))) 452 | return items 453 | 454 | 455 | @plugin.route('/following/') 456 | def following(offset=0): 457 | blogs = {} 458 | litems = [] 459 | blogres = [] 460 | listblogs = [] 461 | litems = [] 462 | name = '' 463 | updated = '' 464 | url = '' 465 | desc = '' 466 | strpage = str(((int(offset) + 50) / 50)) 467 | nextitem = ListItem(label="Next Page -> #{0}".format(int(strpage) + 1), label2="More", icon=__imgnext__, 468 | thumbnail=__imgnext__, path=plugin.url_for(following, offset=int(50 + int(offset)))) 469 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 470 | nextitem.is_folder = True 471 | #litems = [nextitem] 472 | results = following_list(offset=offset) # max not working right now, max=50) 473 | for b in results: 474 | thumb = __imgtumblr__ 475 | try: 476 | thumbd = {} 477 | name = b.get('name', '') 478 | title = b.get('title', '') 479 | desc = b.get('description', '') 480 | url = b.get('url', "http://{0}.tumblr.com".format(name)) 481 | updated = b.get('updated', '') 482 | thumbd = tclient.avatar(name, 128) 483 | if len(thumbd.keys()) > 0: 484 | thumb = thumbd[thumbd.keys()[0]] 485 | if len(thumb) < 1: 486 | if len(b.get('theme', '{}')) > 0: 487 | theme = b.get('theme', '{}') 488 | if len(theme.get('header_image_scaled', '')) > 0: thumb = theme.get('header_image_scaled', '') 489 | iurl = plugin.url_for(endpoint=blogposts, blogname=name, offset=0) 490 | lbl = "{0}\n{1}".format(name, title.encode('latin-1', 'ignore')) 491 | lbl2 = desc.encode('latin-1', 'ignore') 492 | litem = ListItem(label=lbl, label2=lbl2, icon=thumb, thumbnail=thumb, path=iurl) 493 | litem.set_art({'poster': thumb, 'thumbnail': thumb, 'fanart': thumb}) 494 | litem.is_folder = True 495 | litem.playable = False 496 | litems.append(litem) 497 | except: 498 | pass 499 | #items = sorted(litems, key=lambda litems: litems.label2) 500 | # litems.append(nextitem) NO NEXT PAGE TODO: Make max work and paginate results as this is slow 501 | return litems 502 | 503 | 504 | @plugin.route('/blogposts//') 505 | def blogposts(blogname, offset=0): 506 | listposts = [] 507 | lbl = '' 508 | lbl2 = '' 509 | vidurl = '' 510 | results = [] 511 | alltags = [] 512 | litems = [] 513 | if blogname.find('.') != -1: 514 | shortname = blogname.split('.', 1)[-1] 515 | if shortname.find('.') != -1: 516 | blogname = shortname.lsplit('.')[0] 517 | strpage = str((20 + int(offset)) / 20) 518 | nextitem = ListItem(label="Next Page -> #{0}".format(strpage), label2=blogname, icon=__imgnext__, 519 | thumbnail=__imgnext__, 520 | path=plugin.url_for(blogposts, blogname=blogname, offset=int(20 + int(offset)))) 521 | nextitem.set_art({'poster': __imgnext__, 'thumbnail': __imgnext__, 'fanart': __imgnext__}) 522 | nextitem.is_folder = True 523 | #litems = [nextitem] 524 | results = tclient.posts(blogname=blogname, limit=20, offset=int(offset), type='video') 525 | if results is not None: 526 | if len(results.get('posts', '')) > 1: 527 | results = results.get('posts', '') 528 | for post in results: 529 | lbl2 = post.get('blog_name', '') 530 | lbl = post.get('slug', '').replace('-', ' ') 531 | img = post.get('thumbnail_url', __imgtumblr__) 532 | alltags.extend(post.get('tags', [])) 533 | try: 534 | if post.get('slug', '') is not None: 535 | lbl = post.get('slug', '').replace('-', ' ') 536 | if len(post.get('caption', '')) > 0: 537 | lbl = Strip(post.get('caption', '')) 538 | elif len(post.get('summary', '')) > 0: 539 | lbl = post.get('summary', '') 540 | elif len(post.get('source_title', '')) > 0: 541 | lbl = post.get('source_title', '') 542 | else: 543 | lbl = post.get('short_url', '') 544 | if post.get('thumbnail_url', ''): 545 | img = post.get('thumbnail_url', '') 546 | if post.get('video_url', '') is not None: 547 | vidurl = post.get('video_url', '') 548 | except: 549 | plugin.notify(str(repr(post))) 550 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 551 | litem.playable = True 552 | litem.is_folder = False 553 | if len(post.get('date', '')) > 0: 554 | rdate = str(post.get('date', '')).split(' ', 1)[0].strip() 555 | litem.set_info(info_type='video', info_labels={'Date': rdate, 'Duration': post.get('duration', '')}) 556 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 557 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 558 | pathaddlike = plugin.url_for(endpoint=addlike, id=post.get('id','')) 559 | litem.add_context_menu_items([('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)), ]) 560 | litems.append(litem) 561 | else: 562 | litems = [] 563 | backurl = '' 564 | if offset == 0: 565 | backurl = plugin.url_for(endpoint=following, offset=0) 566 | else: 567 | backurl = plugin.url_for(blogposts, blogname=blogname, offset=(int(offset) - 20)) 568 | nextitem = ListItem(label="No Results - GO BACK".format(strpage), label2=blogname, icon=__imgtumblr__, 569 | thumbnail=__imgtumblr__, path=backurl) 570 | nextitem.set_art({'poster': __imgtumblr__, 'thumbnail': __imgtumblr__, 'fanart': __imgtumblr__}) 571 | nextitem.is_folder = True 572 | litems = [nextitem] 573 | savetags(alltags) 574 | litems.append(nextitem) 575 | return litems 576 | 577 | 578 | @plugin.route('/search') 579 | def search(): 580 | # plugin.log.debug(TUMBLRAUTH) 581 | # client = TumblrRestClient(**TUMBLRAUTH) 582 | # info = client.info() 583 | litems = [] 584 | searchtxt = '' 585 | searchquery = '' 586 | offsetnum = 0 587 | searchtxt = plugin.get_setting('lastsearch') 588 | searchtxt = plugin.keyboard(searchtxt, 'Search All Sites', False) 589 | searchquery = searchtxt.replace(' ', '+') 590 | plugin.set_setting(key='lastsearch', val=searchtxt) 591 | results = following_list(offset=offsetnum) 592 | listmatch = [] 593 | max = 20 594 | #if len(results) < 20: 595 | # max = len(results) - 1 596 | for blog in results: 597 | name = blog.get('name', '') 598 | posts = tclient.posts(name, type='video') 599 | for post in posts.get('posts', []): 600 | for k,v in post.items(): 601 | try: 602 | if searchquery.lower() in str(v.encode('latin-1', 'ignore')).lower(): 603 | listmatch.append(post) 604 | break 605 | except: 606 | pass 607 | plugin.notify(msg="Matches: {0}".format(str(len(listmatch)))) 608 | alltags = [] 609 | for post in listmatch: 610 | lbl2 = post.get('blog_name', '') 611 | lbl = post.get('slug', '').replace('-', ' ') 612 | img = post.get('thumbnail_url', __imgtumblr__) 613 | alltags.extend(post.get('tags', [])) 614 | try: 615 | if post.get('slug', '') is not None: 616 | lbl = post.get('slug', '').replace('-', ' ') 617 | if len(post.get('caption', '')) > 0: 618 | lbl = Strip(post.get('caption', '')) 619 | elif len(post.get('summary', '')) > 0: 620 | lbl = post.get('summary', '') 621 | elif len(post.get('source_title', '')) > 0: 622 | lbl = post.get('source_title', '') 623 | else: 624 | lbl = post.get('short_url', '') 625 | if post.get('thumbnail_url', ''): 626 | img = post.get('thumbnail_url', '') 627 | if post.get('video_url', '') is not None: 628 | vidurl = post.get('video_url', '') 629 | except: 630 | plugin.notify(str(repr(post))) 631 | litem = ListItem(label=lbl, label2=lbl2, icon=img, thumbnail=img, path=vidurl) 632 | litem.playable = True 633 | litem.is_folder = False 634 | if len(post.get('date', '')) > 0: 635 | rdate = str(post.get('date', '')).split(' ', 1)[0].strip() 636 | litem.set_info(info_type='video', info_labels={'Date': rdate, 'Duration': post.get('duration', '')}) 637 | litem.set_art({'poster': img, 'thumbnail': img, 'fanart': img}) 638 | pathdl = plugin.url_for(endpoint=download, urlvideo=vidurl) 639 | pathaddlike = plugin.url_for(endpoint=addlike, id=post.get('id', '')) 640 | litem.add_context_menu_items( 641 | [('Download', 'RunPlugin({0})'.format(pathdl)), ('Like', 'RunPlugin({0})'.format(pathaddlike)), ]) 642 | litems.append(litem) 643 | savetags(alltags) 644 | return litems 645 | 646 | 647 | def savetags(taglist=[]): 648 | if not os.path.exists(tagpath): 649 | json.dump([], fp=open(tagpath, mode='w')) 650 | taglist.extend(json.load(open(tagpath, mode='r'))) 651 | alltags = sorted(set(taglist)) 652 | json.dump(alltags, fp=open(tagpath, mode='w')) 653 | 654 | 655 | def Strip(text): 656 | import re 657 | notagre = re.compile(r'<.+?>') 658 | return notagre.sub(' ', text).strip() 659 | 660 | 661 | if __name__ == '__main__': 662 | try: 663 | otoken = plugin.get_setting('oauth_token') 664 | osecret = plugin.get_setting('oauth_secret') 665 | TUMBLRAUTH.update({'oauth_token': otoken, 'oauth_secret': osecret}) 666 | tclient = TumblrRestClient(**TUMBLRAUTH) 667 | info = tclient.info() 668 | if info is not None and 'user' in info.keys(): 669 | APIOK = True 670 | else: 671 | APIOK = False 672 | except: 673 | APIOK = False 674 | try: 675 | TUMBLRAUTH = getoauth() 676 | tclient = TumblrRestClient(**TUMBLRAUTH) 677 | info = tclient.info() 678 | if info is not None and info.get('user', None) is not None: 679 | APIOK = True 680 | else: 681 | APIOK = False 682 | except: 683 | plugin.notify( 684 | msg="Required Tumblr OAUTH token missing..Backup plan!", 685 | title="Tumblr Login Failed", delay=10000) 686 | plugin.log.error(msg="Tumblr API OAuth settings invalid. This addon requires you to authorize this Addon in your Tumblr account and in turn in the settings you must provide the TOKEN and SECRET that Tumblr returns.\nhttps://api.tumblr.com/console/calls/user/info\n\tUse the Consumer Key and Secret from the addon settings to authorize this addon and the OAUTH Token and Secret the website returns must be put into the settings.") 687 | try: # Try an old style API key from off github as a backup so some functionality is provided? 688 | TUMBLRAUTH = dict(consumer_key='5wEwFCF0rbiHXYZQQeQnNetuwZMmIyrUxIePLqUMcZlheVXwc4', 689 | consumer_secret='GCLMI2LnMZqO2b5QheRvUSYY51Ujk7nWG2sYroqozW06x4hWch', 690 | oauth_token='RBesLWIhoxC1StezFBQ5EZf7A9EkdHvvuQQWyLpyy8vdj8aqvU', 691 | oauth_secret='GQAEtLIJuPojQ8fojZrh0CFBzUbqQu8cFH5ejnChQBl4ljJB4a') 692 | TUMBLRAUTH.update({'api_key', 'fuiKNFp9vQFvjLNvx4sUwti4Yb5yGutBN4Xh10LXZhhRKjWlV4'}) 693 | tclient = TumblrRestClient(**TUMBLRAUTH) 694 | except: 695 | plugin.notify(msg="Read Settings for instructions", title="COULDN'T AUTH TO TUMBLR") 696 | viewmode = int(plugin.get_setting('viewmode')) 697 | plugin.run() 698 | plugin.set_content(content='movies') 699 | viewmodel = 51 700 | viewmodet = 500 701 | if str(plugin.request.path).startswith('/taglist/') or plugin.request.path == '/': 702 | viewmodel = int(plugin.get_setting('viewmodelist')) 703 | if viewmodel == 0: viewmodel = 51 704 | plugin.set_view_mode(viewmodel) 705 | else: 706 | viewmodet = int(plugin.get_setting('viewmodethumb')) 707 | if viewmodet == 0: viewmodet = 500 708 | plugin.set_view_mode(viewmodet) 709 | --------------------------------------------------------------------------------