diff --git a/Tested.bundle/Contents/Code/__init__.py b/Tested.bundle/Contents/Code/__init__.py index 1028e7b..071799a 100644 --- a/Tested.bundle/Contents/Code/__init__.py +++ b/Tested.bundle/Contents/Code/__init__.py @@ -1,70 +1,75 @@ -API_PATH = 'http://api.tested.com' -API_KEY = '060b7347a9f8e317a1a9efceea0f8b70bf7018f6' - ART = 'art-default.png' ICON = 'icon-default.png' -def ValidatePrefs(): - link_code = Prefs['link_code'].upper() - if link_code and len(link_code) == 6: - response = JSON.ObjectFromURL(API_PATH + '/validate?link_code=' + link_code + '&format=json') - if 'api_key' in response: - Dict['api_key'] = response['api_key'] - Dict.Save() - +# Category Icons +LIVE_ICON = 'live.png' +LATEST_ICON = 'latest.png' +SCIENCE_ICON = 'science.png' +REVIEW_ICON = 'review.png' +APP_OF_THE_DAY_ICON = 'app-of-the-day.png' +DIY_ICON = 'diy.png' +COFFEE_ICON = 'coffee.png' +HOWTO_ICON = 'howto.png' +MAKERBOT_ICON = 'makerbot.png' +SEARCH_ICON = 'search.png' + +# Creates the main menu (the first menu that pops up when opening the app) @handler('/video/tested', 'Tested') def MainMenu(): - if 'api_key' in Dict: - global API_KEY - API_KEY = Dict['api_key'] - oc = ObjectContainer() - # Live stream - response = JSON.ObjectFromURL(API_PATH + '/chats/?api_key=' + API_KEY + '&format=json') - - if response['status_code'] == 100: - # Revert to the default key - Dict.Reset() - Dict.Save() - - chats = response['results'] - for chat in chats: - url = 'http://www.justin.tv/widgets/live_embed_player.swf?channel=' + chat['channel_name'] + '&auto_play=true&start_volume=25' - if chat['password']: - url += '&publisherGuard=' + chat['password'] - + # Checks to see if a livestream is going on, since the Tested API is no longer around it checks the Justin TV API + # Assumes live streams are on the "tested" channel + LIVE_CHANNEL = 'tested' + response = JSON.ObjectFromURL('http://api.justin.tv/api/stream/list.json?channel=' + LIVE_CHANNEL) + + if( len(response) > 0): + + stream = response[0] #This should be fine as there can only ever be one live stream going per channel + + url = 'http://www.justin.tv/widgets/live_embed_player.swf?channel=' + LIVE_CHANNEL + '&auto_play=true&start_volume=25' + title = stream['title'] + oc.add( VideoClipObject( key=WebVideoURL(url), - title='LIVE: ' + chat['title'], - summary=chat['deck'], + title='LIVE: ' + title, source_title='Justin.tv', - thumb=chat['image']['super_url'], + thumb=R(LIVE_ICON), art=R(ART), - rating_key=chat['channel_name'] + rating_key=LIVE_CHANNEL ) ) + oc.add( DirectoryObject( key='/video/tested/videos', title='Latest', - summary='Watch the newest stuff.', - thumb=R(ICON), + summary='', + thumb=R(LATEST_ICON), art=R(ART) ) ) - - categories = JSON.ObjectFromURL(API_PATH + '/video_types/?api_key=' + API_KEY + '&format=json')['results'] - + + # The categories were chosen using a script to see what the most popular tags were on tested youtube videos + # Script can be found here: https://gist.github.com/2940575 + categories = [ + ('Science & Technology', 'Tech', '', R(SCIENCE_ICON)), + ('Reviews', 'review', '', R(REVIEW_ICON)), + ('App Of The Day', 'app,of,the,day', '', R(APP_OF_THE_DAY_ICON)), + ('DIY', 'diy', '', R(DIY_ICON)), + ('Coffee', 'coffee', '', R(COFFEE_ICON)), + ('Howtos', 'howto', '', R(HOWTO_ICON)), + ('Makerbot', 'makerbot', '', R(MAKERBOT_ICON)) + ] for cat in categories: oc.add( DirectoryObject( - key='/video/tested/videos/?cat_id=' + str(cat['id']), - title=cat['name'], - summary=cat['deck'], - thumb=R(ICON), + key='/video/tested/videos/?cat_id=' + str(cat[1]), + title=cat[0], + summary='', + thumb=cat[3], art=R(ART) ) ) @@ -74,64 +79,133 @@ def MainMenu(): key=Callback(Videos), title='Search', prompt='Search', - thumb=R(ICON), - art=R(ART) - ) - ) - - oc.add( - PrefsObject( - title='Preferences', - thumb=R(ICON), + thumb=R(SEARCH_ICON), art=R(ART) ) ) return oc - + +# Handles when a category is selected from the main menu @route('/video/tested/videos') -def Videos(cat_id=None, query=None): - if 'api_key' in Dict: - global API_KEY - API_KEY = Dict['api_key'] +def Videos(cat_id=None, query=None, page = 1): + + # Apparently plex *always* passes parameters to these functions as strings + # So have to 'unconvert' the parameters + page = int(page) + + if query == '': + query = None + + if cat_id == '': + cat_id = None + + # Change this to change how many results are displayed per page, maximum is 50 according to youtube API + MAXRESULTS = 20 oc = ObjectContainer() + Log(str(page)) + url_suffix = '&start-index=' + str((page - 1) * MAXRESULTS + 1) + url_suffix += '&max-results=' + str(MAXRESULTS) + if query: - videos = JSON.ObjectFromURL(API_PATH + '/search/?api_key=' + API_KEY + '&resources=video&query=' + query + '&format=json')['results'] + videos = JSON.ObjectFromURL('http://gdata.youtube.com/feeds/api/videos?alt=json&author=testedcom&prettyprint=true&v=2' + url_suffix + '&q=' + query.replace(' ', '%20')) elif cat_id: - videos = JSON.ObjectFromURL(API_PATH + '/videos/?api_key=' + API_KEY + '&video_type=' + cat_id + '&sort=-publish_date&format=json')['results'] + videos = JSON.ObjectFromURL('http://gdata.youtube.com/feeds/api/videos?alt=json&author=testedcom&prettyprint=true&v=2' + url_suffix + '&orderby=published&category=' + cat_id) else: - videos = JSON.ObjectFromURL(API_PATH + '/videos/?api_key=' + API_KEY + '&sort=-publish_date&format=json')['results'] - - if Prefs['quality'] == 'Auto': - if 'hd_url' in videos[0]: - quality = 'hd_url' - else: - quality = 'high_url' - else: - quality = Prefs['quality'].lower() + '_url' - - for vid in videos: - if 'wallpaper_image' not in vid or not vid['wallpaper_image']: # or whatever it gets called - vid_art = R(ART) - else: - vid_art = vid['wallpaper_image'] - - if quality == 'hd_url': - url = vid[quality] + '&api_key=' + API_KEY - else: - url = vid[quality] - - oc.add( - VideoClipObject( - key=url, - title=vid['name'], - summary=vid['deck'], - thumb=vid['image']['super_url'], - art=vid_art, - rating_key=vid['id'] - ) - ) + videos = JSON.ObjectFromURL('https://gdata.youtube.com/feeds/api/users/testedcom/uploads?alt=json&prettyprint=true&v=2' + url_suffix ) + + + # The following tidbit was inspired by the youtube plugin that plex uses + if videos['feed'].has_key('entry'): + + for video in videos['feed']['entry']: + + if CheckRejectedEntry(video): + continue + + video_url = None + for video_links in video['link']: + if video_links['type'] == 'text/html': + video_url = video_links['href'] + break + + if video_url is None: + Log('Found video that had no URL') + continue + + video_title = video['media$group']['media$title']['$t'] + thumb = video['media$group']['media$thumbnail'][2]['url'] + duration = int(video['media$group']['yt$duration']['seconds']) * 1000 + + summary = None + try: summary = video['media$group']['media$description']['$t'] + except: pass + + rating = None + try: rating = float(video['gd$rating']['average']) * 2 + except: pass + + date = None + try: date = Datetime.ParseDate(video['published']['$t'].split('T')[0]) + except: + try: date = Datetime.ParseDate(video['updated']['$t'].split('T')[0]) + except: pass + + oc.add( + VideoClipObject( + url=video_url + "&hd=1", + title=video_title, + summary=summary, + thumb=Callback(GetThumb, url = thumb), + art=R(ART), + rating = rating, + originally_available_at = date + ) + ) + + # Handles pagination if there are more results that were not shown + if videos['feed'].has_key('openSearch$totalResults'): + total_results = int(videos['feed']['openSearch$totalResults']['$t']) + items_per_page = int(videos['feed']['openSearch$itemsPerPage']['$t']) + start_index = int(videos['feed']['openSearch$startIndex']['$t']) + + if not query: + query = '' + + if not cat_id: + cat_id = '' + + if (start_index + items_per_page) < total_results: + oc.add(DirectoryObject(key = Callback(Videos, cat_id = cat_id, query = query, page = page + 1), title = 'Next')) return oc + +# Retrives the thumbnail of a video from the given url, caching it +def GetThumb(url): + try: + data = HTTP.Request(url, cacheTime = CACHE_1WEEK).content + return DataObject(data, 'image/jpeg') + except: + Log.Exception("Error when attempting to get the associated thumb") + return Redirect(R(ICON)) + +# Checks to see if the video passed is a valid video (not deleted, rejected, restricted, etc +def CheckRejectedEntry(entry): + try: + status_name = entry['app$control']['yt$state']['name'] + + if status_name in ['deleted', 'rejected', 'failed']: + return True + + if status_name == 'restricted': + status_reason = entry['app$control']['yt$state']['reasonCode'] + + if status_reason in ['private', 'requesterRegion']: + return True + + except: + pass + + return False \ No newline at end of file diff --git a/Tested.bundle/Contents/DefaultPrefs.json b/Tested.bundle/Contents/DefaultPrefs.json index b366385..32960f8 100644 --- a/Tested.bundle/Contents/DefaultPrefs.json +++ b/Tested.bundle/Contents/DefaultPrefs.json @@ -1,15 +1,2 @@ [ - { - "id": "link_code", - "label": "Link Code", - "type": "text", - "default": "" - }, - { - "id": "quality", - "type": "enum", - "label": "Quality", - "values": ["Auto", "Low", "High", "HD"], - "default": "Auto" - } ] \ No newline at end of file diff --git a/Tested.bundle/Contents/Info.plist b/Tested.bundle/Contents/Info.plist index c7dde26..254e3d9 100644 --- a/Tested.bundle/Contents/Info.plist +++ b/Tested.bundle/Contents/Info.plist @@ -5,7 +5,7 @@ CFBundleIdentifier com.plexapp.plugins.tested PlexClientPlatforms - MacOSX,iOS,Android,LGTV + MacOSX,iOS,Android,LGTV,Windows PlexFrameworkVersion 2 PlexPluginClass diff --git a/Tested.bundle/Contents/Resources/app-of-the-day.png b/Tested.bundle/Contents/Resources/app-of-the-day.png new file mode 100644 index 0000000..1951d1d Binary files /dev/null and b/Tested.bundle/Contents/Resources/app-of-the-day.png differ diff --git a/Tested.bundle/Contents/Resources/art-default.png b/Tested.bundle/Contents/Resources/art-default.png index 1df9dca..e0c4bb8 100644 Binary files a/Tested.bundle/Contents/Resources/art-default.png and b/Tested.bundle/Contents/Resources/art-default.png differ diff --git a/Tested.bundle/Contents/Resources/coffee.png b/Tested.bundle/Contents/Resources/coffee.png new file mode 100644 index 0000000..68d3aa1 Binary files /dev/null and b/Tested.bundle/Contents/Resources/coffee.png differ diff --git a/Tested.bundle/Contents/Resources/diy.png b/Tested.bundle/Contents/Resources/diy.png new file mode 100644 index 0000000..9a2e613 Binary files /dev/null and b/Tested.bundle/Contents/Resources/diy.png differ diff --git a/Tested.bundle/Contents/Resources/howto.png b/Tested.bundle/Contents/Resources/howto.png new file mode 100644 index 0000000..313db7f Binary files /dev/null and b/Tested.bundle/Contents/Resources/howto.png differ diff --git a/Tested.bundle/Contents/Resources/icon-default.png b/Tested.bundle/Contents/Resources/icon-default.png index c531c7e..246c7aa 100644 Binary files a/Tested.bundle/Contents/Resources/icon-default.png and b/Tested.bundle/Contents/Resources/icon-default.png differ diff --git a/Tested.bundle/Contents/Resources/latest.png b/Tested.bundle/Contents/Resources/latest.png new file mode 100644 index 0000000..ae9b80a Binary files /dev/null and b/Tested.bundle/Contents/Resources/latest.png differ diff --git a/Tested.bundle/Contents/Resources/live.png b/Tested.bundle/Contents/Resources/live.png new file mode 100644 index 0000000..35b5ea6 Binary files /dev/null and b/Tested.bundle/Contents/Resources/live.png differ diff --git a/Tested.bundle/Contents/Resources/makerbot.png b/Tested.bundle/Contents/Resources/makerbot.png new file mode 100644 index 0000000..a27395b Binary files /dev/null and b/Tested.bundle/Contents/Resources/makerbot.png differ diff --git a/Tested.bundle/Contents/Resources/review.png b/Tested.bundle/Contents/Resources/review.png new file mode 100644 index 0000000..2d4f2ce Binary files /dev/null and b/Tested.bundle/Contents/Resources/review.png differ diff --git a/Tested.bundle/Contents/Resources/science.png b/Tested.bundle/Contents/Resources/science.png new file mode 100644 index 0000000..3593b23 Binary files /dev/null and b/Tested.bundle/Contents/Resources/science.png differ diff --git a/Tested.bundle/Contents/Resources/search.png b/Tested.bundle/Contents/Resources/search.png new file mode 100644 index 0000000..de3bbdf Binary files /dev/null and b/Tested.bundle/Contents/Resources/search.png differ