├── 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 |
--------------------------------------------------------------------------------