diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c91b435
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,37 @@
+# Compiled source #
+###################
+*.com
+*.class
+*.dll
+*.exe
+*.o
+*.so
+
+# Packages #
+############
+# it's better to unpack these files and commit the raw source
+# git has its own built in compression methods
+*.7z
+*.dmg
+*.gz
+*.iso
+*.jar
+*.rar
+*.tar
+*.zip
+
+# Logs and databases #
+######################
+*.log
+*.sql
+*.sqlite
+
+# OS generated files #
+######################
+.DS_Store
+.DS_Store?
+._*
+.Spotlight-V100
+.Trashes
+ehthumbs.db
+Thumbs.db
\ No newline at end of file
diff --git a/Contents/Code/PHCategories.py b/Contents/Code/PHCategories.py
new file mode 100644
index 0000000..a3bdca0
--- /dev/null
+++ b/Contents/Code/PHCategories.py
@@ -0,0 +1,25 @@
+from PHCommon import *
+
+PH_CATEGORIES_URL = BASE_URL + '/categories'
+PH_CATEGORIES_ALPHABETICAL_URL = PH_CATEGORIES_URL + '?o=al'
+
+@route(ROUTE_PREFIX + '/categories')
+def BrowseCategories(title=L("DefaultBrowseCategoriesTitle"), url = PH_CATEGORIES_ALPHABETICAL_URL):
+
+ # Create a dictionary of menu items
+ browseCategoriesMenuItems = OrderedDict()
+
+ # Get list of categories
+ categories = SharedCodeService.PHCategories.GetCategories(url)
+
+ # Loop through all categories
+ for category in categories:
+
+ # Add a menu item for the category
+ browseCategoriesMenuItems[category["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + category["url"]},
+ 'directoryObjectArgs': {'thumb': category["thumbnail"]}
+ }
+
+ return GenerateMenu(title, browseCategoriesMenuItems)
\ No newline at end of file
diff --git a/Contents/Code/PHChannels.py b/Contents/Code/PHChannels.py
new file mode 100644
index 0000000..0cd8a82
--- /dev/null
+++ b/Contents/Code/PHChannels.py
@@ -0,0 +1,59 @@
+from PHCommon import *
+
+PH_CHANNELS_URL = BASE_URL + '/channels'
+PH_CHANNEL_SEARCH_URL = PH_CHANNELS_URL + '/search?channelSearch=%s'
+MAX_CHANNELS_PER_PAGE = 36
+
+@route(ROUTE_PREFIX + '/channels')
+def BrowseChannels(title="DefaultBrowseChannelsTitle"):
+
+ # Create a dictionary of menu items
+ browseChannelsMenuItems = OrderedDict([
+ ('Search Channels', {'function':SearchChannels, 'search':True, 'directoryObjectArgs':{'prompt':'Search for...','summary':'Enter Channel Search Terms'}}),
+ ('Most Popular', {'function':ListChannels, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_CHANNELS_URL, {'o':'rk'})}}),
+ ('Trending', {'function':ListChannels, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_CHANNELS_URL, {'o':'tr'})}}),
+ ('Most Recent', {'function':ListChannels, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_CHANNELS_URL, {'o':'mr'})}}),
+ ('A-Z', {'function':ListChannels, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_CHANNELS_URL, {'o':'al'})}})
+ ])
+
+ return GenerateMenu(title, browseChannelsMenuItems)
+
+@route(ROUTE_PREFIX + '/channels/list')
+def ListChannels(title, url = PH_CHANNELS_URL, page=1):
+
+ # Create a dictionary of menu items
+ listChannelsMenuItems = OrderedDict()
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # Get list of channels
+ channels = SharedCodeService.PHChannels.GetChannels(url)
+
+ # Loop through all channels
+ for channel in channels:
+
+ # Add a menu item for the channel
+ listChannelsMenuItems[channel["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + channel["url"] + '/videos'},
+ 'directoryObjectArgs': {'thumb': channel["thumbnail"]}
+ }
+
+ # There is a slight change that this will break... If the number of videos returned in total is divisible by MAX_VIDEOS_PER_PAGE with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(channels) == MAX_CHANNELS_PER_PAGE):
+ listChannelsMenuItems['Next Page'] = {'function':ListChannels, 'functionArgs':{'title':title, 'url':url, 'page':int(page)+1}, 'nextPage':True}
+
+ return GenerateMenu(title, listChannelsMenuItems)
+
+@route(ROUTE_PREFIX + '/channels/search')
+def SearchChannels(query):
+
+ # Format the query for use in PornHub's search
+ formattedQuery = SharedCodeService.PHCommon.FormatStringForSearch(query, "+")
+
+ try:
+ return ListChannels(title='Search Results for ' + query, url=PH_CHANNEL_SEARCH_URL % query)
+ except:
+ return ObjectContainer(header='Search Results', message="No search results found", no_cache=True)
\ No newline at end of file
diff --git a/Contents/Code/PHCommon.py b/Contents/Code/PHCommon.py
new file mode 100644
index 0000000..8d40a7d
--- /dev/null
+++ b/Contents/Code/PHCommon.py
@@ -0,0 +1,419 @@
+from collections import OrderedDict
+
+ROUTE_PREFIX = '/video/pornhub'
+
+BASE_URL = 'http://pornhub.com'
+PH_VIDEO_URL = BASE_URL + '/video'
+PH_VIDEO_SEARCH_URL = PH_VIDEO_URL + '/search?search=%s'
+
+PH_USER_HOVER_URL = BASE_URL + '/user/hover?id=%s'
+
+MAX_VIDEOS_PER_PAGE = 44
+MAX_VIDEOS_PER_PAGE_PAGE_ONE = 32
+MAX_VIDEOS_PER_SEARCH_PAGE = 20
+MAX_VIDEOS_PER_CHANNEL_PAGE = 36
+MAX_VIDEOS_PER_PORNSTAR_PAGE = 36
+MAX_VIDEOS_PER_USER_PAGE = 48
+
+SORT_ORDERS = OrderedDict([
+ ('Most Recent', {'o':'mr'}),
+ ('Most Viewed - All Time', {'o':'mv', 't':'a'}),
+ ('Most Viewed - This Month', {'o':'mv', 't':'m'}),
+ ('Most Viewed - This Week', {'o':'mv', 't':'w'}),
+ ('Most Viewed - Today', {'o':'mv', 't':'t'}),
+ ('Top Rated - All Time', {'o':'tr', 't':'a'}),
+ ('Top Rated - This Month', {'o':'tr', 't':'m'}),
+ ('Top Rated - This Week', {'o':'tr', 't':'w'}),
+ ('Top Rated - Today', {'o':'tr', 't':'t'}),
+ ('Most Discussed - All Time', {'o':'md', 't':'a'}),
+ ('Most Discussed - This Month', {'o':'md', 't':'m'}),
+ ('Most Discussed - This Week', {'o':'md', 't':'w'}),
+ ('Most Discussed - Today', {'o':'md', 't':'t'}),
+ ('Being Watched', {'o':'bw'}),
+ ('Longest', {'o':'lg'})
+])
+
+CHANNEL_VIDEOS_SORT_ORDERS = OrderedDict([
+ ('Most Recent', {'o':'da'}),
+ ('Most Viewed', {'o':'vi'}),
+ ('Top Rated', {'o':'ra'})
+])
+
+PORNSTAR_VIDEOS_SORT_ORDERS = OrderedDict([
+ ('Recently Featured', {}),
+ ('Most Viewed', {'o':'mv'}),
+ ('Top Rated', {'o':'tr'}),
+ ('Longest', {'o':'lg'}),
+ ('Newest', {'o':'cm'})
+])
+
+@route(ROUTE_PREFIX + '/videos/browse')
+def BrowseVideos(title=L("DefaultBrowseVideosTitle"), url = PH_VIDEO_URL, sortOrders = SORT_ORDERS):
+
+ # If sorting channels, use a different dictionary of sort orders
+ if ("/channels/" in url):
+ sortOrders = CHANNEL_VIDEOS_SORT_ORDERS
+ elif ("/pornstar/" in url):
+ sortOrders = PORNSTAR_VIDEOS_SORT_ORDERS
+
+ # Create a dictionary of menu items
+ browseVideosMenuItems = OrderedDict()
+
+ # Add the sorting options
+ for sortTitle, urlParams in sortOrders.items():
+
+ # Add a menu item for the category
+ browseVideosMenuItems[sortTitle] = {'function':ListVideos, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(url, urlParams)}}
+
+ return GenerateMenu(title, browseVideosMenuItems)
+
+@route(ROUTE_PREFIX + '/videos/list')
+def ListVideos(title=L("DefaultListVideosTitle"), url=PH_VIDEO_URL, page=1, pageLimit = MAX_VIDEOS_PER_PAGE_PAGE_ONE):
+
+ # Create the object to contain all of the videos
+ oc = ObjectContainer(title2 = title)
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # This could definitely be handled more gracefully. But it works for now
+ if ("/video/search" in url):
+ pageLimit = MAX_VIDEOS_PER_SEARCH_PAGE
+ elif ("/channels/" in url):
+ pageLimit = MAX_VIDEOS_PER_CHANNEL_PAGE
+ elif ("/pornstar/" in url):
+ pageLimit = MAX_VIDEOS_PER_PORNSTAR_PAGE
+ elif ("/users/" in url):
+ pageLimit = MAX_VIDEOS_PER_USER_PAGE
+ elif ("/video" in url and page > 1):
+ # In the Browse All Videos and Categories menus, they display MAX_VIDEOS_PER_PAGE_PAGE_ONE on page one, and MAX_VIDEOS_PER_PAGE from page two onward
+ pageLimit = MAX_VIDEOS_PER_PAGE
+
+ # Get list of categories
+ videos = SharedCodeService.PHCommon.GetVideos(url)
+
+ # Loop through the videos in the page
+ for video in videos:
+
+ # Check for relative URLs
+ if (video["url"].startswith('/')):
+ video["url"] = BASE_URL + video["url"]
+
+ # Make sure the last step went smoothly (this is probably redundant but oh well)
+ if (video["url"].startswith(BASE_URL)):
+
+ # Add a Directory Object for the video to the Object Container
+ oc.add(DirectoryObject(
+ key = Callback(VideoMenu, url=video["url"], title=video["title"], duration=video["duration"]),
+ title = video["title"],
+ thumb = video["thumbnail"],
+ duration = video["duration"]
+ ))
+
+ # There is a slight change that this will break... If the number of videos returned in total is divisible by MAX_VIDEOS_PER_PAGE with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(videos) == int(pageLimit)):
+ oc.add(NextPageObject(
+ key = Callback(ListVideos, title=title, url=url, page = int(page)+1, pageLimit=int(pageLimit)),
+ title = 'Next Page'
+ ))
+
+ return oc
+
+@route(ROUTE_PREFIX + '/videos/menu')
+def VideoMenu(url, title=L("DefaultVideoMenuTitle"), duration=0):
+ # Create the object to contain all of the videos options
+ oc = ObjectContainer(title2 = title, no_cache=True)
+
+ # Create the Video Clip Object
+ vco = URLService.MetadataObjectForURL(url)
+
+ # As I am calling MetadataObjectForURL from the URL Service, it only returns the metadata, it doesn't contain the URL
+ vco.url = url
+
+ # Overide the title
+ vco.title = "Play Video"
+
+ if (int(duration) > 0):
+ vco.duration = int(duration)
+
+ # Add the Video Clip Object
+ oc.add(vco)
+
+ # Get the HTML of the site
+ html = HTML.ElementFromURL(url)
+
+ # Get the video meta data
+ videoMetaData = SharedCodeService.PHCommon.GetVideoMetaDataJSON(htmlElement=html)
+
+ # Check to see if Thumbnails are enabled in the video sub menu in the Preferences, and also if the Thumbnail metadata exists
+ if (Prefs["videoMenuShowThumbnails"] and videoMetaData["thumbs"] and videoMetaData["thumbs"]["urlPattern"]):
+ oc.add(PhotoAlbumObject(
+ key = Callback(VideoThumbnails, url=url),
+ rating_key = url + " - Thumbnails",
+ title = "Thumbnails",
+ summary = "Tiled thumbnails from this video"
+ ))
+
+ # Check to see if Uploaders are enabled in the video sub menu in the Preferences
+ if (Prefs["videoMenuShowUploader"]):
+ # Use xPath to extract the uploader of the video
+ uploader = html.xpath("//div[contains(@class, 'video-info-row')]/div[contains(@class, 'usernameWrap')]")
+
+ # Make sure one is returned
+ if (len(uploader) > 0):
+ # Get the link within
+ uploaderLink = uploader[0].xpath("./a")
+
+ # Make sure it exists
+ if (len(uploaderLink) > 0):
+ uploaderURL = BASE_URL + uploaderLink[0].xpath("./@href")[0]
+ uploaderName = uploaderLink[0].xpath("./text()")[0]
+
+ uploaderType = uploader[0].xpath("./@data-type")[0]
+
+ # Check to see if the video is listed under a channel or a user
+ if (uploaderType == "channel"):
+ channelID = uploader[0].xpath("./@data-channelid")[0]
+
+ # Get the porn star hover meta data
+ channelHoverMetaData = SharedCodeService.PHChannels.GetChannelHoverMetaData(channelID)
+
+ oc.add(DirectoryObject(
+ key = Callback(BrowseVideos, url=uploaderURL + '/videos', title=uploaderName),
+ title = uploaderName,
+ summary = "Channel this video appears in",
+ thumb = channelHoverMetaData["thumbnail"]
+ ))
+ elif (uploaderType == "user"):
+ pass
+
+ # Check to see if Porn Stars are enabled in the video sub menu in the Preferences
+ if (Prefs["videoMenuShowPornStars"]):
+ # Use xPath to extract a list of porn stars in the video
+ pornStars = html.xpath("//div[contains(@class, 'pornstarsWrapper')]/a[contains(@class, 'pstar-list-btn')]")
+
+ # Check how any porn stars are returned.
+ # If just one, then display a Directory Object pointing to the porn star
+ if (len(pornStars) == 1):
+ oc.add(GenerateVideoPornStarDirectoryObject(pornStars[0]))
+
+ # If more than one, create a Directory Object to another menu where all porn stars will be listed
+ elif (len(pornStars) > 1):
+ oc.add(DirectoryObject(
+ key = Callback(GenerateVideoPornStarMenu, url=url),
+ title = "Porn Stars",
+ summary = "Porn Stars that appear in this video"
+ ))
+
+
+ # Check to see if Related Videos are enabled in the video sub menu in the Preferences
+ if (Prefs["videoMenuShowRelatedVideos"]):
+ # Use xPath to extract the related videos
+ relatedVideos = html.xpath("//ul[@id='relatedVideosCenter' or @id='relateRecommendedItems']//li[contains(@class, 'videoblock')]/div[contains(@class, 'wrap')]/div[contains(@class, 'phimage')]")
+
+ if (len(relatedVideos) > 0):
+ relatedVideosThumb = relatedVideos[0].xpath("./div[contains(@class, 'img')]/a/img/@data-mediumthumb")[0]
+
+ # Add the Related Videos Directory Object
+ oc.add(DirectoryObject(
+ key = Callback(RelatedVideos, url=url),
+ title = "Related Videos",
+ summary = "Videos related to this video",
+ thumb = relatedVideosThumb
+ ))
+
+
+ # Check to see if Playlists are enabled in the video sub menu in the Preferences
+ if (Prefs["videoMenuShowPlaylists"]):
+ # Fetch playlists containing the video (if any)
+ playlists = html.xpath("//ul[contains(@class, 'playlist-listingSmall')]/li/div[contains(@class, 'wrap')]")
+
+ if (len(playlists) > 0):
+ playlistsThumb = playlists[0].xpath("./div[contains(@class, 'linkWrapper')]/img/@data-mediumthumb")[0]
+
+ oc.add(DirectoryObject(
+ key = Callback(PlaylistsContainingVideo, url=url),
+ title = "Playlists",
+ summary = "Playlists that contain this video",
+ thumb = playlistsThumb
+ ))
+
+ # Check to see if Action is enabled in the video sub menu in the Preferences
+ if (Prefs["videoMenuShowAction"]):
+ if (videoMetaData["actionTags"]):
+ oc.add(DirectoryObject(
+ key = Callback(VideoActions, url=url),
+ title = "Action",
+ summary = "Timestamps of when actions (e.g. different positions) happen in this video"
+ ))
+
+ return oc
+
+@route(ROUTE_PREFIX + '/search')
+def SearchVideos(query):
+
+ # Format the query for use in PornHub's search
+ formattedQuery = SharedCodeService.PHCommon.FormatStringForSearch(query, "+")
+
+ try:
+ return ListVideos(title='Search Results For ' + query, url=PH_VIDEO_SEARCH_URL % formattedQuery)
+ except:
+ return ObjectContainer(header='Search Results', message="No search results found", no_cache=True)
+
+@route(ROUTE_PREFIX + '/video/pornstars')
+def GenerateVideoPornStarMenu(url, title="Porn Stars"):
+ # Create the object to contain all of the porn stars in the video
+ oc = ObjectContainer(title2 = title)
+
+ # Get the HTML of the site
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of porn stars in the video
+ pornStars = html.xpath("//div[contains(@class, 'pornstarsWrapper')]/a[contains(@class, 'pstar-list-btn')]")
+
+ if (len(pornStars) > 0):
+ for pornStar in pornStars:
+ oc.add(GenerateVideoPornStarDirectoryObject(pornStar))
+
+ return oc
+
+# This function takes markup of a porn star from a video page and creates a Directory Object for it
+def GenerateVideoPornStarDirectoryObject(pornStarElement):
+ pornStarID = pornStarElement.xpath("./@data-id")[0]
+ pornStarURL = BASE_URL + pornStarElement.xpath("./@href")[0]
+ pornStarName = pornStarElement.xpath("./text()")[0]
+
+ # Get the porn star hover meta data
+ pornStarHoverMetaData = SharedCodeService.PHPornStars.GetPornStarHoverMetaData(pornStarID)
+
+ return DirectoryObject(
+ key = Callback(BrowseVideos, url=pornStarURL, title=pornStarName),
+ title = pornStarName,
+ summary = "Porn Star appearing in this video",
+ thumb = pornStarHoverMetaData["thumbnail"]
+ )
+
+@route(ROUTE_PREFIX + '/video/thumbnails')
+def VideoThumbnails(url, title="Thumbnails"):
+ # Create the object to contain the thumbnails
+ oc = ObjectContainer(title2=title)
+
+ # Get the video thumbnail URLs
+ thumbnailURLs = SharedCodeService.PHCommon.GetVideoThumbnailURLs(url)
+
+ for i, thumbnailURL in enumerate(thumbnailURLs):
+
+ oc.add(PhotoObject(
+ key = thumbnailURL,
+ rating_key = thumbnailURL,
+ title = "Thumbnail #" + str(i + 1),
+ thumb = thumbnailURL
+ ))
+
+ return oc
+
+@route(ROUTE_PREFIX + '/video/related')
+def RelatedVideos(url, title="Related Videos"):
+ # Create the object to contain the related videos
+ oc = ObjectContainer(title2=title)
+
+ # Get the video thumbnail URLs
+ relatedVideos = SharedCodeService.PHCommon.GetRelatedVideos(url)
+
+ # Loop through related videos
+ for relatedVideo in relatedVideos:
+
+ oc.add(DirectoryObject(
+ key = Callback(VideoMenu, url=BASE_URL + relatedVideo["url"], title=relatedVideo["title"]),
+ title = relatedVideo["title"],
+ summary = relatedVideo["title"],
+ thumb = relatedVideo["thumbnail"]
+ ))
+
+ return oc
+
+@route(ROUTE_PREFIX + '/video/playlists')
+def PlaylistsContainingVideo(url, title="Playlists Containing Video"):
+ # Create the object to contain the playlists
+ oc = ObjectContainer(title2=title)
+
+ # Get the playlists containing the video
+ playlists = SharedCodeService.PHCommon.GetPlaylistsContainingVideo(url)
+
+ # Loop through playlists
+ for playlist in playlists:
+
+ oc.add(DirectoryObject(
+ key = Callback(BrowseVideos, url=BASE_URL + playlist["url"], title=playlist["title"]),
+ title = playlist["title"],
+ thumb = playlist["thumbnail"]
+ ))
+
+ return oc
+
+@route(ROUTE_PREFIX + '/video/actions')
+def VideoActions(url, title="Actions", header=None, message=None, replace_parent=None):
+ # Create the object to contain the actions
+ oc = ObjectContainer(title2=title)
+
+ if (header):
+ oc.header = header
+ if (message):
+ oc.message = message
+ if (replace_parent):
+ oc.replace_parent = replace_parent
+
+ # Get the playlists containing the video
+ actions = SharedCodeService.PHCommon.GetVideoActions(url)
+
+ for action in actions:
+
+ actionSummary = action["title"] + " starts at " + action["timestamp"]
+
+ oc.add(DirectoryObject(
+ key = Callback(VideoActions, url=url, title=title, header=action["title"], message=actionSummary, replace_parent=True),
+ title = action["timestamp"] + ": " + action["title"],
+ summary = actionSummary
+ ))
+
+ return oc
+
+def GenerateMenu(title, menuItems, no_cache=False):
+ # Create the object to contain the menu items
+ oc = ObjectContainer(title2=title, no_cache=no_cache)
+
+ # Loop through the menuItems dictionary
+ for menuTitle, menuData in menuItems.items():
+ # Create empty dictionaries to hold the arguments for the Directory Object and the Function
+ directoryObjectArgs = {}
+ functionArgs = {}
+
+ # See if any Directory Object arguments are present in the menu data
+ if ('directoryObjectArgs' in menuData):
+ # Merge dictionaries
+ directoryObjectArgs.update(menuData['directoryObjectArgs'])
+
+ # Check to see if the menu item is a search menu item
+ if ('search' in menuData and menuData['search'] == True):
+ directoryObject = InputDirectoryObject(title=menuTitle, **directoryObjectArgs)
+ # Check to see if the menu item is a next page item
+ elif ('nextPage' in menuData and menuData['nextPage'] == True):
+ directoryObject = NextPageObject(title=menuTitle, **directoryObjectArgs)
+ # Otherwise, use a basic Directory Object
+ else:
+ directoryObject = DirectoryObject(title=menuTitle, **directoryObjectArgs)
+ functionArgs['title'] = menuTitle
+
+ # See if any Function arguments are present in the menu data
+ if ('functionArgs' in menuData):
+ # Merge dictionaries
+ functionArgs.update(menuData['functionArgs'])
+
+ # Set the Directory Object key to the function from the menu data, passing along any additional function arguments
+ directoryObject.key = Callback(menuData['function'], **functionArgs)
+
+ # Add the Directory Object to the Object Container
+ oc.add(directoryObject)
+
+ return oc
diff --git a/Contents/Code/PHMembers.py b/Contents/Code/PHMembers.py
new file mode 100644
index 0000000..0e85d8e
--- /dev/null
+++ b/Contents/Code/PHMembers.py
@@ -0,0 +1,268 @@
+from PHCommon import *
+
+PH_DISCOVER_MEMBERS_URL = BASE_URL + '/user/discover'
+PH_SEARCH_MEMBERS_URL = BASE_URL + '/user/search?username=%s'
+
+# Only the Members search results page has 42 results. The other Member pages have 48 results, but don't feature pagination
+PH_MAX_MEMBERS_PER_PAGE = 42
+PH_MAX_MEMBERS_PER_MEMBER_SUBSCRIBERS_PAGE = 100
+PH_MAX_MEMBERS_PER_MEMBER_SUBSCRIPTIONS_PAGE = 100
+PH_MAX_MEMBERS_PER_MEMBER_FRIENDS_PAGE = 100
+PH_MAX_MEMBER_CHANNELS_PER_PAGE = 8
+
+@route(ROUTE_PREFIX + '/members')
+def BrowseMembers(title=L("DefaultBrowseMembersTitle"), url=PH_DISCOVER_MEMBERS_URL):
+
+ # Create a dictionary of menu items
+ browseMembersMenuItems = OrderedDict([
+ ('Search Members', {'function':SearchMembers, 'search':True, 'directoryObjectArgs':{'prompt':'Search for...','summary':"Enter member's username"}})
+ ])
+
+ # Get list of sort orders
+ sortOrders = SharedCodeService.PHMembers.GetMemberSortOrders(url)
+
+ # Loop through all sort orders
+ for sortOrder in sortOrders:
+
+ # Add a menu item for the sort order
+ browseMembersMenuItems[sortOrder["title"]] = {
+ 'function': ListMembers,
+ 'functionArgs': {'url': BASE_URL + sortOrder["url"]}
+ }
+
+ return GenerateMenu(title, browseMembersMenuItems)
+
+@route(ROUTE_PREFIX + '/members/list')
+def ListMembers(title, url=PH_DISCOVER_MEMBERS_URL, page=1, pageLimit=PH_MAX_MEMBERS_PER_PAGE):
+
+ # Create a dictionary of menu items
+ listMembersMenuItems = OrderedDict()
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # Get list of members
+ members = SharedCodeService.PHMembers.GetMembers(url)
+
+ # Loop through all members
+ for member in members:
+
+ # Add a menu item for the member
+ listMembersMenuItems[member["title"]] = {
+ 'function': MemberMenu,
+ 'functionArgs': {'url': BASE_URL + member["url"], 'username':member["title"]},
+ 'directoryObjectArgs': {'thumb': member["thumbnail"]}
+ }
+
+ # There is a slight change that this will break... If the number of members returned in total is divisible by pageLimit with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(members) == int(pageLimit)):
+ listMembersMenuItems['Next Page'] = {'function':ListMembers, 'functionArgs':{'title':title, 'url':url, 'page':int(page)+1, 'pageLimit':int(pageLimit)}, 'nextPage':True}
+
+ return GenerateMenu(title, listMembersMenuItems)
+
+@route(ROUTE_PREFIX + '/members/search')
+def SearchMembers(query):
+
+ # Format the query for use in PornHub's search
+ formattedQuery = SharedCodeService.PHCommon.FormatStringForSearch(query, "+")
+
+ try:
+ return ListMembers(title='Search Results for ' + query, url=PH_SEARCH_MEMBERS_URL % formattedQuery)
+ except:
+ return ObjectContainer(header='Search Results', message="No search results found", no_cache=True)
+
+@route(ROUTE_PREFIX + '/members/menu')
+def MemberMenu(title, url, username):
+
+ # Get the HTML of the Member's spash page, as well as their Video and Playlist pages
+ memberHTML = HTML.ElementFromURL(url)
+
+ # Create a dictionary of menu items
+ memberMenuItems = OrderedDict([
+ ('Public Videos', {'function':ListVideos, 'functionArgs':{'title':username + "'s Public Videos", 'url':url + '/videos/public'}}),
+ ('Favorite Videos', {'function':ListVideos, 'functionArgs':{'title':username + "'s Favorite Videos", 'url':url + '/videos/favorites'}}),
+ ('Watched Videos', {'function':ListVideos, 'functionArgs':{'title':username + "'s Watched Videos", 'url':url + '/videos/recent'}}),
+ ('Public Playlists', {'function':ListPlaylists, 'functionArgs':{'title':username + "'s Public Playlists", 'url':url + '/playlists/public'}}),
+ ('Favorite Playlists', {'function':ListPlaylists, 'functionArgs':{'title':username + "'s Favorite Playlists", 'url':url + '/playlists/favorites'}}),
+ ('Channels', {'function':ListMemberChannels, 'functionArgs':{'title':username + "'s Channels", 'url':url + '/channels'}}),
+ ('Channel Subscriptions', {'function':ListMemberSubscribedChannels, 'functionArgs':{'title':username + "'s Channel Subscriptions", 'url':url + '/channel_subscriptions'}}),
+ ('Porn Star Subscriptions', {'function':ListMemberSubscribedPornStars, 'functionArgs':{'title':username + "'s Porn Star Subscriptions", 'url':url + '/pornstar_subscriptions'}}),
+ ('Subscribers', {'function':ListMembers, 'functionArgs':{'title':username + "'s Subscribers", 'url':url + '/subscribers', 'pageLimit':PH_MAX_MEMBERS_PER_MEMBER_SUBSCRIBERS_PAGE}}),
+ ('Member Subscriptions', {'function':ListMembers, 'functionArgs':{'title':username + "'s Member Subscriptions", 'url':url + '/subscriptions', 'pageLimit':PH_MAX_MEMBERS_PER_MEMBER_SUBSCRIPTIONS_PAGE}}),
+ ('Friends', {'function':ListMembers, 'functionArgs':{'title':username + "'s Friends", 'url':url + '/friends', 'pageLimit':PH_MAX_MEMBERS_PER_MEMBER_FRIENDS_PAGE}})
+ ])
+
+ # This dictionary will hold the conditons on which we want to display Member menu options
+ memberMenuChecks = {
+ "Public Videos": {
+ "xpath": "//section[@id='profileVideos']//ul[contains(@class,'videos')]/li[contains(@class,'videoblock')]",
+ "htmlElement": memberHTML
+ },
+ "Favorite Videos": None,
+ "Watched Videos": None,
+ "Public Playlists": {
+ "xpath": "//section[@id='playlistsSidebar']//ul[contains(@class,'user-playlist')]/li[contains(@id,'playlist_')]",
+ "htmlElement": memberHTML
+ },
+ "Favorite Playlists": None,
+ "Channels": {
+ "xpath": "//div[contains(@class,'channelSubWidgetContainer')]/ul/li[contains(@class,'channelSubChannelWig')]",
+ "htmlElement": memberHTML
+ },
+ "Channel Subscriptions": {
+ "xpath": "//div[contains(@class,'userWidgetContainer')]/ul/li[contains(@class,'userChannelWig')]",
+ "htmlElement": memberHTML
+ },
+ "Porn Star Subscriptions": {
+ "xpath": "//section[@id='sidebarPornstars']//ul[contains(@class,'pornStarSideBar')]/li[contains(@class,'pornstarsElements')]",
+ "htmlElement": memberHTML
+ },
+ "Subscribers": {
+ "xpath": "//ul[contains(@class,'subViewsInfoContainer')]/li[a[span[contains(@class,'connections')][contains(text(),'subscriber')]]]/a/span[contains(@class,'number')][not(text()='0')]",
+ "htmlElement": memberHTML
+ },
+ "Member Subscriptions": {
+ "xpath": "//section[@id='profileSubscriptions']//ul/li[contains(@class,'subscriptionsElement')]",
+ "htmlElement": memberHTML
+ },
+ "Friends": {
+ "xpath": "//ul[contains(@class,'subViewsInfoContainer')]/li[a[span[contains(@class,'connections')][contains(text(),'friend')]]]/a/span[contains(@class,'number')][not(text()='0')]",
+ "htmlElement": memberHTML
+ }
+ }
+
+ # These overrides perform a more accurate check, however they all require an extra HTTP request
+ memberMenuPreferenceOverrides = {
+ "memberMenuAccurateVideos": {
+ 'urlSuffix': '/videos',
+ 'checks': [
+ {'key':'Public Videos', 'xpath':"//section[@id='videosTab']//nav[contains(@class,'sectionMenu')]/ul/li/a[text()='Public']"},
+ {'key':'Favorite Videos', 'xpath':"//section[@id='videosTab']//nav[contains(@class,'sectionMenu')]/ul/li/a[text()='Favorites']"},
+ {'key':'Watched Videos', 'xpath':"//section[@id='videosTab']//nav[contains(@class,'sectionMenu')]/ul/li/a[text()='Watched']"}
+ ]
+ },
+ "memberMenuAccuratePlaylists": {
+ 'urlSuffix': '/playlists',
+ 'checks': [
+ {'key':'Public Playlists', 'xpath':"//nav[contains(@class,'sectionMenu')]/ul/li/a[text()='Public']"},
+ {'key':'Favorite Playlists', 'xpath':"//nav[contains(@class,'sectionMenu')]/ul/li/a[text()='Favorites']"}
+ ]
+ },
+ "memberMenuAccurateSubscribers": {
+ 'urlSuffix': '/subscribers',
+ 'checks': [
+ {'key':'Subscribers', 'xpath':"//ul[contains(@class, 'userWidgetWrapperGrid')]/li"}
+ ]
+ },
+ "memberMenuAccurateMemberSubscriptions": {
+ 'urlSuffix': '/subscriptions',
+ 'checks': [
+ {'key':'Member Subscriptions', 'xpath':"//ul[contains(@class, 'userWidgetWrapperGrid')]/li"}
+ ]
+ },
+ "memberMenuAccurateFriends": {
+ 'urlSuffix': '/friends',
+ 'checks': [
+ {'key':'Friends', 'xpath':"//ul[contains(@class, 'userWidgetWrapperGrid')]/li"}
+ ]
+ }
+ }
+
+ # Loop through Preference overrides
+ for key in memberMenuPreferenceOverrides:
+ # Check to see if the Preference is set
+ if (Prefs[key]):
+ # Get the HTML of the page
+ memberMenuPreferenceOverrideHTML = HTML.ElementFromURL(url + memberMenuPreferenceOverrides[key]["urlSuffix"])
+
+ # Loop through the checks
+ for check in memberMenuPreferenceOverrides[key]["checks"]:
+ # Override the menu check
+ memberMenuChecks[check['key']] = {
+ "xpath": check['xpath'],
+ "htmlElement": memberMenuPreferenceOverrideHTML
+ }
+
+ # Loop through Member menu option conditons
+ for key in memberMenuChecks:
+ # Make sure the check exists
+ if (memberMenuChecks[key] is not None):
+ # Attempt to get the element from the page
+ elements = memberMenuChecks[key]["htmlElement"].xpath(memberMenuChecks[key]["xpath"])
+
+ if (len(elements) == 0):
+ # If no elements are found, do not display the Member menu option
+ del memberMenuItems[key]
+ else:
+ del memberMenuItems[key]
+
+ return GenerateMenu(title, memberMenuItems, no_cache=True)
+
+@route(ROUTE_PREFIX + '/members/channels')
+def ListMemberChannels(url, title="Member Channels", page=1):
+
+ # Create a dictionary of menu items
+ memberChannelMenuItems = OrderedDict()
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # Get list of channels
+ channels = SharedCodeService.PHMembers.GetMemberChannels(url)
+
+ for channel in channels:
+
+ # Add a menu item for the channel
+ memberChannelMenuItems[channel["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + channel["url"], 'title':channel["title"]},
+ 'directoryObjectArgs': {'thumb': channel["thumbnail"]}
+ }
+
+ # There is a slight change that this will break... If the number of Channels returned in total is divisible by PH_MAX_MEMBER_CHANNELS_PER_PAGE with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(channels) == PH_MAX_MEMBER_CHANNELS_PER_PAGE):
+ memberChannelMenuItems['Next Page'] = {'function':ListMemberChannels, 'functionArgs':{'title':title, 'url':url, 'page':int(page)+1}, 'nextPage':True}
+
+ return GenerateMenu(title, memberChannelMenuItems)
+
+@route(ROUTE_PREFIX + '/members/channels/subscribed')
+def ListMemberSubscribedChannels(url, title="Member's Subscribed Channels"):
+
+ # Create a dictionary of menu items
+ memberSubscribedChannelMenuItems = OrderedDict()
+
+ # Get list of channels
+ channels = SharedCodeService.PHMembers.GetMemberSubscribedChannels(url)
+
+ for channel in channels:
+
+ # Add a menu item for the Channel
+ memberSubscribedChannelMenuItems[channel["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + channel["url"], 'title':channel["title"]},
+ 'directoryObjectArgs': {'thumb': channel["thumbnail"]}
+ }
+
+ return GenerateMenu(title, memberSubscribedChannelMenuItems)
+
+@route(ROUTE_PREFIX + '/members/pornstars')
+def ListMemberSubscribedPornStars(url, title="Member's Subscribed Porn Stars"):
+
+ # Create a dictionary of menu items
+ memberSubscribedPornStarsMenuItems = OrderedDict()
+
+ # Get list of porn stars
+ pornStars = SharedCodeService.PHMembers.GetMemberSubscribedPornStars(url)
+
+ for pornStar in pornStars:
+
+ # Add a menu item for the Porn Star
+ memberSubscribedPornStarsMenuItems[pornStar["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + pornStar["url"], 'title':pornStar["title"]},
+ 'directoryObjectArgs': {'thumb': pornStar["thumbnail"]}
+ }
+
+ return GenerateMenu(title, memberSubscribedPornStarsMenuItems)
\ No newline at end of file
diff --git a/Contents/Code/PHPlaylists.py b/Contents/Code/PHPlaylists.py
new file mode 100644
index 0000000..2735623
--- /dev/null
+++ b/Contents/Code/PHPlaylists.py
@@ -0,0 +1,58 @@
+from PHCommon import *
+
+PH_PLAYLISTS_URL = BASE_URL + '/playlists'
+PH_PLAYLIST_URL = BASE_URL + '/playlist'
+
+MAX_PLAYLISTS_PER_PAGE = 36
+
+@route(ROUTE_PREFIX + '/playlists')
+def BrowsePlaylists(title=L("DefaultBrowsePlaylistsTitle")):
+
+ # Create a dictionary of menu items
+ browsePlaylistsMenuItems = OrderedDict([
+ ('Most Recent', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mr'})}}),
+ ('Top Rated - All Time', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'tr', 't':'a'})}}),
+ ('Top Rated - Monthly', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'tr', 't':'m'})}}),
+ ('Top Rated - Weekly', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'tr', 't':'w'})}}),
+ ('Top Rated - Daily', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'tr', 't':'t'})}}),
+ ('Most Viewed - All Time', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mv', 't':'a'})}}),
+ ('Most Viewed - Monthly', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mv', 't':'m'})}}),
+ ('Most Viewed - Weekly', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mv', 't':'w'})}}),
+ ('Most Viewed - Daily', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mv', 't':'t'})}}),
+ ('Most Favorited', {'function':ListPlaylists, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PLAYLISTS_URL, {'o':'mf'})}})
+ ])
+
+ return GenerateMenu(title, browsePlaylistsMenuItems)
+
+@route(ROUTE_PREFIX + '/playlists/list')
+def ListPlaylists(title, url = PH_PLAYLISTS_URL, page=1):
+
+ # Create a dictionary of menu items
+ listPlaylistsMenuItems = OrderedDict()
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # Get list of playlists
+ playlists = SharedCodeService.PHPlaylists.GetPlaylists(url)
+
+ # Loop through all playlists
+ for playlist in playlists:
+
+ # Make sure Playlist isn't empty
+ if (playlist["isEmpty"] == False):
+
+ # Add a menu item for the playlist
+ # TODO: I am currently using the playlist title as a key, however these aren't guarenteed to be unique.
+ listPlaylistsMenuItems[playlist["title"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + playlist["url"]},
+ 'directoryObjectArgs': {'thumb': playlist["thumbnail"]}
+ }
+
+ # There is a slight change that this will break... If the number of playlists returned in total is divisible by MAX_PLAYLISTS_PER_PAGE with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(playlists) == MAX_PLAYLISTS_PER_PAGE):
+ listPlaylistsMenuItems['Next Page'] = {'function':ListPlaylists, 'functionArgs':{'title':title, 'url':url, 'page':int(page)+1}, 'nextPage':True}
+
+ return GenerateMenu(title, listPlaylistsMenuItems)
\ No newline at end of file
diff --git a/Contents/Code/PHPornStars.py b/Contents/Code/PHPornStars.py
new file mode 100644
index 0000000..38fcf15
--- /dev/null
+++ b/Contents/Code/PHPornStars.py
@@ -0,0 +1,67 @@
+from PHCommon import *
+
+PH_PORNSTARS_URL = BASE_URL + '/pornstars'
+PH_PORNSTARS_SEARCH_URL = PH_PORNSTARS_URL + '/search?search=%s'
+
+MAX_PORNSTARS_PER_PAGE = 28
+
+@route(ROUTE_PREFIX + '/pornstars')
+def BrowsePornStars(title=L("DefaultBrowsePornStarsTitle")):
+
+ # Create a dictionary of menu items
+ browsePornStarsMenuItems = OrderedDict([
+ ('Search Porn Stars', {'function':SearchPornStars, 'search':True, 'directoryObjectArgs':{'prompt':'Search for...','summary':'Enter Porn Star Search Terms'}}),
+ ('Most Popular - All Time', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'t':'a'})}}),
+ ('Most Popular - Monthly', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'t':'m'})}}),
+ ('Most Popular - Weekly', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'t':'w'})}}),
+ ('Most Viewed - All Time', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'mv', 't':'a'})}}),
+ ('Most Viewed - Monthly', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'mv', 't':'m'})}}),
+ ('Most Viewed - Weekly', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'mv', 't':'w'})}}),
+ ('Most Viewed - Daily', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'mv', 't':'t'})}}),
+ ('Top Trending', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'t'})}}),
+ ('Most Subscribed', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'ms'})}}),
+ ('Alphabetical', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'a'})}}),
+ ('Number of Videos', {'function':ListPornStars, 'functionArgs':{'url':SharedCodeService.PHCommon.AddURLParameters(PH_PORNSTARS_URL, {'o':'nv'})}})
+ ])
+
+ return GenerateMenu(title, browsePornStarsMenuItems)
+
+@route(ROUTE_PREFIX + '/pornstars/list')
+def ListPornStars(title, url = PH_PORNSTARS_URL, page=1):
+
+ # Create a dictionary of menu items
+ listPornStarsMenuItems = OrderedDict()
+
+ # Add the page number into the query string
+ if (int(page) != 1):
+ url = SharedCodeService.PHCommon.AddURLParameters(url, {'page':str(page)})
+
+ # Get list of porn stars
+ pornStars = SharedCodeService.PHPornStars.GetPornStars(url)
+
+ # Loop through all channels
+ for pornStar in pornStars:
+
+ # Add a menu item for the porn star
+ listPornStarsMenuItems[pornStar["name"]] = {
+ 'function': BrowseVideos,
+ 'functionArgs': {'url': BASE_URL + pornStar["url"]},
+ 'directoryObjectArgs': {'thumb': pornStar["thumbnail"]}
+ }
+
+ # There is a slight change that this will break... If the number of videos returned in total is divisible by MAX_VIDEOS_PER_PAGE with no remainder, there could possibly be no additional page after. This is unlikely though and I'm too lazy to handle it.
+ if (len(pornStars) == MAX_PORNSTARS_PER_PAGE):
+ listPornStarsMenuItems['Next Page'] = {'function':ListPornStars, 'functionArgs':{'title':title, 'url':url, 'page':int(page)+1}, 'nextPage':True}
+
+ return GenerateMenu(title, listPornStarsMenuItems)
+
+@route(ROUTE_PREFIX + '/pornstars/search')
+def SearchPornStars(query):
+
+ # Format the query for use in PornHub's search
+ formattedQuery = SharedCodeService.PHCommon.FormatStringForSearch(query, "+")
+
+ try:
+ return ListPornStars(title='Search Results for ' + query, url=PH_PORNSTARS_SEARCH_URL % formattedQuery)
+ except:
+ return ObjectContainer(header='Search Results', message="No search results found", no_cache=True)
\ No newline at end of file
diff --git a/Contents/Code/__init__.py b/Contents/Code/__init__.py
index 907a02c..d3374d4 100644
--- a/Contents/Code/__init__.py
+++ b/Contents/Code/__init__.py
@@ -1,387 +1,59 @@
-from PMS import *
-from PMS.Objects import *
-from PMS.Shortcuts import *
-from urlparse import urljoin
-import time
-from exceptions import IndexError
-####################################################################################################
-
-PLUGIN_TITLE = 'Pornhub'
-STEALTH_TITLE = 'System Stats'
-PLUGIN_PREFIX = '/video/pornhub'
-
-BASE_URL = 'http://www.pornhub.com'
-CATEGORIES = '%s/categories' % BASE_URL
-AMFPROXY = 'http://amfproxy.plexapp.net/pornhub/%s.xml'
-SORT_ORDER = [
- ['Most Recent', 'o=mr'],
- ['Most Viewed - All Time', 'o=mv&t=a'],
- ['Most Viewed - This Month', 'o=mv&t=m'],
- ['Most Viewed - This Week', 'o=mv&t=w'],
- ['Most Viewed - Today', 'o=mv&t=t'],
- ['Top Rated - All Time', 'o=tr&t=a'],
- ['Top Rated - This Month', 'o=tr&t=m'],
- ['Top Rated - This Week', 'o=tr&t=w'],
- ['Top Rated - Today', 'o=tr&t=t'],
- ['Most Discussed - All Time', 'o=md&t=a'],
- ['Most Discussed - This Month', 'o=md&t=m'],
- ['Most Discussed - This Week', 'o=md&t=w'],
- ['Most Discussed - Today', 'o=md&t=t'],
- ['Being Watched', 'o=bw'],
- ['Longest', 'o=lg']]
-
-# Default artwork and icons
-PLUGIN_ARTWORK = 'art-default.png'
-PLUGIN_ICON_DEFAULT = 'icon-default.png'
-STEALTH_ARTWORK = 'art-stealth.png'
-STEALTH_ICON = 'icon-stealth.png'
-
-####################################################################################################
-
-# Lazy Loader
-global pages
-pages = list()
-global dirItems
-dirItems = MediaContainer()
-global parseThread
-parseThread = None
-global noMorePages
-noMorePages = True
-global noMoreMeta
-noMoreMeta = True
-global metaThread
-global lastMatchKey
-lastMatchKey = None
-
-####################################################################################################
-
-def LL(sender, pageGetter, parser, metaGetter, url, matchKey, title1=None, title2=None, viewGroup=None, contextMenu=None, replaceParent=False, **kwargs):
- global pages, dirItems, parseThread, metaThread, noMorePages, noMoreMeta, lastMatchKey, parseThreadShouldDie, metaThreadShouldDie
- Log(matchKey)
- Log(lastMatchKey)
-
- if pageGetter == 'getURLs':
- pageHnd = getURLs
- elif pageGetter == 'getVideos':
- pageHnd = getVideos
- else:
- raise
-
- if parser == 'getVideos':
- parseHnd = getVideos
- else:
- raise
-
- if metaGetter == 'getMeta':
- metaHnd = getMeta
- elif metaGetter == 'NOP':
- metaHnd = NOP
-
-
- if matchKey != lastMatchKey:
- lastMatchKey = matchKey
- parseThreadShouldDie = True
- metaThreadShouldDie = True
- while (parseThreadShouldDie and not noMorePages) or (metaThreadShouldDie and not noMoreMeta):
- Log('Waiting for old threads to die')
- Log([parseThreadShouldDie, noMorePages, metaThreadShouldDie, noMoreMeta])
- time.sleep(1)
- Log('Threads are all dead')
- pages = list()
- dirItems = MediaContainer(title1=title1, title2=title2, viewGroup=viewGroup, contextMenu=contextMenu, replaceParent=replaceParent)
- parseThread = None
- if parseThread == None:
- parseThreadShouldDie = False
- metaThreadShouldDie = False
- pages = pageHnd(url, **kwargs)
- noMorePages = False
- noMoreMeta = False
- dirItems.autoRefresh=5
- parseThread = Thread.Create(parseEach, parseHnd, **kwargs)
- metaThread = Thread.Create(getMetaEach, metaHnd)
- time.sleep(3)
- return(dirItems)
-
-def parseEach(parser, **kwargs):
- global pages, dirItems, noMorePages, parseThread, parseThreadShouldDie
- while True:
- if parseThreadShouldDie:
- parseThreadShouldDie = False
- return
- try:
- url = pages[0]
- del pages[0]
- except:
- Log('Out of pages')
- noMorePages = True
- return
- else:
- try:
- parser(url, **kwargs)
- except:
- Log('parser raised an exception')
-
-def getMetaEach(metaGetter):
- global dirItems, noMorePages, noMoreMeta, metaThreadShouldDie
- itemIndex = 0
- while True:
- if metaThreadShouldDie:
- metaThreadShouldDie = False
- return
- try:
- dirItem = dirItems[itemIndex]
- except IndexError:
- if noMorePages:
- Log('No more videos')
- del dirItems.autoRefresh
- noMoreMeta = True
- return
- else:
- Log('Waiting for videos')
- time.sleep(1)
- else:
- itemIndex += 1
- try:
- metaGetter(dirItem)
- except:
- Log('metaGetter raised an exception')
-
-####################################################################################################
-
-def P(pref, default=''):
- p = Prefs.Get(pref)
- if p == None:
- return default
- else:
- return p
-
-def V(val, default=''):
- if val == None:
- return default
- else:
- return val
-####################################################################################################
-
-def Start():
- if P('Stealth', False):
- Plugin.AddPrefixHandler(PLUGIN_PREFIX, MainMenu, STEALTH_TITLE, STEALTH_ICON, STEALTH_ARTWORK)
- else:
- Plugin.AddPrefixHandler(PLUGIN_PREFIX, MainMenu, PLUGIN_TITLE, PLUGIN_ICON_DEFAULT, PLUGIN_ARTWORK)
- Plugin.AddViewGroup('List', viewMode='List', mediaType='items')
-
- # Set the default MediaContainer attributes
- MediaContainer.title1 = PLUGIN_TITLE
- MediaContainer.viewGroup = 'List'
- MediaContainer.art = R(PLUGIN_ARTWORK)
-
- Plugin.AddViewGroup('_List', viewMode='List', mediaType='items')
- Plugin.AddViewGroup('_InfoList', viewMode='InfoList', mediaType='items')
- Plugin.AddViewGroup('_Pictures', viewMode='Pictures', mediaType='items')
- Plugin.AddViewGroup('_Wall Stream', viewMode='WallStream', mediaType='items')
- Plugin.AddViewGroup('_Cover Flow', viewMode='Coverflow', mediaType='items')
-
- # Set the default cache time
- HTTP.SetCacheTime(CACHE_1HOUR)
-
-####################################################################################################
-
-def CreatePrefs():
- Prefs.Add(id='Stealth', type='bool', default=False, label='Stealth Mode')
- Prefs.Add(id='catView', type='enum', default='List', label='Default Category View', values='List|InfoList|Pictures|Wall Stream|Cover Flow')
- Prefs.Add(id='videoView', type='enum', default='List', label='Default Video View', values='List|InfoList|Pictures|Wall Stream|Cover Flow')
- sortValues = ''
- for sort, key in SORT_ORDER:
- sortValues += sort + '|'
- sortValues = sortValues + 'Prompt'
- Prefs.Add(id='pageCount', type='text', default='1', label='Pages (26 videos each)')
- Prefs.Add(id='sortOrder', type='enum', default='Prompt', label='Default Sort Order', values=sortValues)
- for category in XML.ElementFromURL(CATEGORIES, isHTML=True, cacheTime=CACHE_1DAY, errors='ignore').xpath('//li[@class="cat_pic"]//strong/text()'):
- Prefs.Add(id=category.strip().replace(' ', '_').replace('/', '_'), type='bool', default=True, label='Show ' + category.strip())
-
-def CreateDict():
- Dict.Set('oldStealthSetting', False)
-
-####################################################################################################
-
-def getURLs(url, sortURL, **kwargs):
- if url.find('?') == -1:
- totalUrl = urljoin(BASE_URL, url + '?' + sortURL)
- else:
- totalUrl = urljoin(BASE_URL, url + '&' + sortURL)
- videoPage = XML.ElementFromURL(totalUrl, isHTML=True, errors='ignore')
- try:
- pageCount = int(videoPage.xpath('//span[text()="Last"]/parent::*')[0].get('href').split('=')[-1])
- except:
- pageCount = len(videoPage.xpath('//ul[@class="pagination"]/li')) - 1
- if pageCount == -1:
- pageCount = 1
- pages = list()
- for p in range(1, pageCount + 1):
- pages.append(totalUrl + '&page=' + str(p))
- return pages
-
-def getVideos(url, **kwargs):
- global dirItems
- Log('getVideos for ' + url)
- for video in XML.ElementFromURL(url, isHTML=True, errors='ignore').xpath('//div[@class="wrap"]'):
- title = video.xpath('.//a[@class="title"]')[0].text.strip()
- duration = TimeToSeconds(video.xpath('.//var[@class="duration"]')[0].text) * 1000
- thumb = video.xpath('.//img')[0].get('src')
- rating = float(video.xpath('.//div[starts-with(@class,"rating-container")]/div[@class="value"]')[0].text.split('%')[0]) * 2
-
- added = L('Added: %s') % video.xpath('.//var[@class="added"]')[0].text
- views = L('Views: %s views') % video.xpath('.//span[@class="views"]/var')[0].text
-
- viewkey = video.xpath('.//a')[0].get('href').split('viewkey=')
- premium = video.xpath('.//a')[0].get('href').find('view_video_2.php')
- private = thumb.find('private-video')
-
- if len(viewkey) > 1 and premium < 0 and private < 0:
- videoURL = 'http://www.pornhub.com/view_video.php?viewkey=' + viewkey[1]
- dirItems.Append(Function(VideoItem(getVideo, title=title, summary=added + '\n' + views, duration=duration, thumb=thumb, art=None, rating=rating), videoURL=videoURL))
-
-def getMeta(dirItem):
- Log(dirItem.__dict__)
- metaURL = dirItem._Function__kwargs['videoURL']
- Log('Getting metadata for ' + metaURL)
-
- metaPage = None
- tries = 3
- while metaPage == None and tries != 0:
- metaPage = XML.ElementFromURL(metaURL, True, errors='ignore', cacheTime=CACHE_1MONTH)
- tries -= 1
- if metaPage == None:return
-
- summary = dirItem.summary + '\n'
- users = metaPage.xpath('//a[starts-with(@href,"/user/")]')
- if len(users) != 0:
- summary += 'From: ' + V(users[0].text) + '\n'
- stars = metaPage.xpath('//a[starts-with(@href,"/video/search?pornstar")]')
- if len(stars) != 0:
- summary += 'Pornstars: '
- for star in stars:
- summary += V(star.text) +', '
- summary = summary[:-2] + '\n'
- tags = metaPage.xpath('//a[starts-with(@href,"/video/search?search=")]')
- if len(tags) != 0:
- summary += 'Tags: '
- for tag in tags:
- summary += V(tag.text) + ', '
- summary = summary[:-2] + '\n'
- dirItem.summary = summary
-
-def getVideo(sender, videoURL):
- js = XML.ElementFromURL(videoURL, True).xpath('//div[@id="playerDiv_1"]/following-sibling::script')[0].text
- for line in js.split('\n'):
- if '"video_url"' in line:
- url = line.split('"')[-2]
- return Redirect(url)
-
-def MainMenu():
- stealthSetting = Prefs.Get('Stealth')
- if Dict.Get('oldStealthSetting') != stealthSetting:
- Dict.Set('oldStealthSetting', stealthSetting)
- time.sleep(5)
- Log('Stealth Mode toggled, Restarting')
- Plugin.Restart()
-
- dir = MediaContainer(noCache=True)
- dir.viewGroup = '_' + Prefs.Get('catView')
-
- sortName, sortURL = getSort()
- # 'All' item
- if sortName == '':
- dir.Append(Function(DirectoryItem(SortOrder, title='All', thumb=R(PLUGIN_ICON_DEFAULT)), url='/video?c=', title2='All'))
- else:
- dir.Append(Function(DirectoryItem(LL, title='All', thumb=R(PLUGIN_ICON_DEFAULT)), pageGetter='getURLs', parser='getVideos', metaGetter='getMeta', title1=PLUGIN_TITLE, title2='All', url='/video?c=', sortURL=sortURL, matchKey=['/video?c=', sortURL]))
-
- for category in XML.ElementFromURL(CATEGORIES, isHTML=True, cacheTime=CACHE_1DAY, errors='ignore').xpath('//li[@class="cat_pic"]'):
- url = category.xpath('./a')[0].get('href')
-
- title = category.xpath('.//strong')[0].text.strip()
- thumb = category.xpath('./a/img')[0].get('src')
- if Prefs.Get(title.strip().replace(' ', '_').replace('/', '_')):
- if sortName == '':
- dir.Append(Function(DirectoryItem(SortOrder, title=title, thumb=thumb), url=url, title2=title, viewGroup='_' + Prefs.Get('videoView')))
- else:
- dir.Append(Function(DirectoryItem(LL, title=title, thumb=thumb), pageGetter='getURLs', parser='getVideos', metaGetter='getMeta', title1=PLUGIN_TITLE, title2=title, url=url, sortURL=sortURL, viewGroup='_' + Prefs.Get('videoView'), matchKey=[url, sortURL]))
- #dir.Append(Function(DirectoryItem(LL, title=L('Users')), pageGetter=getURLs, parser=getUsers, metaGetter=NOP, title1=PLUGIN_TITLE, title2='Users', url='http://www.pornhub.com/user/search', sortURL='o=recent_users', viewGroup='_' + Prefs.Get('videoView'), matchKey=['http://www.pornhub.com/user/search', sortURL]))
-
- dir.Append(Function(InputDirectoryItem(InputVideoList, title=L("Search Videos ..."), prompt=L("Search on Pornhub"), subtitle = L('Search by keyword in pornhub archive'), summary = 'You can type in any word you want to search content for. This can be tags, names, ...' ), title1 = PLUGIN_TITLE, title2 = 'Search - "%s"', url = 'http://www.pornhub.com/video/search?search=%s', sortURL=sortURL))
- #Add search item
- #dir.Append(Function(InputDirectoryItem(InputUserList, title=L("Search Users ..."), prompt=L("Search User by name"), subtitle = L('Search by name in pornhub user list')), title1 = PLUGIN_TITLE, title2 = 'Search User - "%s"', url = BASE_URL + '/user/search?username=%s', sortURL=sortURL))
- dir.Append(PrefsItem('Preferences', thumb=R('icon-prefs.png')))
- return dir
-
-####################################################################################################
-
-def SortOrder(sender, url, title2, viewGroup):
- dir = MediaContainer(title2=title2)
-
- for (sortTitle,sortURL) in SORT_ORDER:
- dir.Append(Function(DirectoryItem(LL, title=sortTitle, thumb=R(PLUGIN_ICON_DEFAULT)), pageGetter='getURLs', parser='getVideos', metaGetter='getMeta', title1=title2, title2=sortTitle, url=url, sortURL=sortURL, viewGroup=viewGroup, matchKey=[url, sortURL]))
-
- return dir
-
-####################################################################################################
-
-def InputVideoList(sender, query, title1, title2, url, sortURL, page=1):
- Log('(PLUG-IN) **==> ENTER Search on PornHub')
- title2 = title2 % query
- query = String.Quote(query, usePlus=True)
- url = url % query
- return LL(sender, title1=title1, title2=title2, url=url, sortURL=sortURL, pageGetter=getURLs, parser=getVideos, metaGetter=getMeta, matchKey=['searchVideos', query])
-
-####################################################################################################
-
-def TimeToSeconds(timecode):
- seconds = 0
- duration = timecode.split(':')
- duration.reverse()
-
- for i in range(0, len(duration)):
- seconds += int(duration[i]) * (60**i)
-
- return seconds
-
-####################################################################################################
-
-def getSort():
- sort = Prefs.Get('sortOrder')
- for name, url in SORT_ORDER:
- if sort == name: return [name, url]
- return ['','']
-
-####################################################################################################
-
-# TODO: write user metadata getter
-
-def getUsers(url, **kwargs):
- global dirItems
- for user in XML.ElementFromURL(url, isHTML=True, errors='ignore').xpath('//div[@class="user-box"]'):
- name = user.xpath('./a/span')[0].text
- link = user.xpath('./a')[0].get('href')
- thumb = user.xpath('./a/img')[0].get('src')
- #userid = link.split('user/')[1]
-
- if len(link) > 1:
- userURL = BASE_URL + link #+ '/videos/recent?'
- dirItems.Append(Function(DirectoryItem(getUsers2, title=name, thumb=thumb), title1=PLUGIN_TITLE, userName=name, url=userURL, viewGroup='_' + Prefs.Get('videoView')))
-
-def getUsers2(sender, url, title1, userName, viewGroup):
- dir = MediaContainer(title1=title1, title2=userName, viewGroup=viewGroup)
- dir.Append(Function(DirectoryItem(LL, title='Favourites'), pageGetter='getURLs', parser='getVideos', metaGetter='NOP', title1=PLUGIN_TITLE, title2='Favourites - ' + userName, url=url + '/videos/favorites', sortURL='o=mr', viewGroup='_' + Prefs.Get('videoView'), matchKey=['getUsersFaves', userName]))
- dir.Append(Function(DirectoryItem(LL, title='Recents'), pageGetter='getURLs', parser='getVideos', metaGetter='NOP', title1=PLUGIN_TITLE, title2='Recents - ' + userName, url=url + '/videos/recent?', sortURL='', viewGroup='_' + Prefs.Get('videoView'), matchKey=['getUsersRecents', userName]))
- return dir
-
-def NOP(*args, **kwargs):
- pass
-
-####################################################################################################
-
-def InputUserList(sender, query, title1, title2, url, sortURL, page=1):
- Log('(PLUG-IN) **==> ENTER Search on PornHub')
- title2 = title2 % query
- query = String.Quote(query, usePlus=True)
- url = url % query
- return LL(sender, url=url, pageGetter=getURLs, parser=getUsers, metaGetter=NOP, title1=PLUGIN_TITLE, title2=query, sortURL='', viewGroup='_' + Prefs.Get('videoView'), matchKey=['searchUsers', query])
+from PHCommon import *
+from PHCategories import *
+from PHChannels import *
+from PHPornStars import *
+from PHPlaylists import *
+from PHMembers import *
+
+NAME = L("ChannelTitle")
+
+ART = 'art-' + Prefs["channelBackgroundArt"]
+ICON = 'icon-' + Prefs["channelIconArt"]
+
+def Start():
+
+ # Set the defaults for Object Containers
+ ObjectContainer.art = R(ART)
+ ObjectContainer.title1 = NAME
+
+ # Set the defaults of Directory Objects
+ DirectoryObject.thumb = R(ICON)
+ PhotoAlbumObject.thumb = R(ICON)
+
+ # Set the default language
+ Locale.DefaultLocale = "en"
+
+ # Set the cache lifespan
+ HTTP.CacheTime = CACHE_1HOUR * 2
+
+ # Set the user agent
+ HTTP.Headers['User-Agent'] = 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:42.0) Gecko/20100101 Firefox/42.0'
+
+def ValidatePrefs():
+ ART = 'art-' + Prefs["channelBackgroundArt"]
+ ObjectContainer.art = R(ART)
+
+ ICON = 'icon-' + Prefs["channelIconArt"]
+ DirectoryObject.thumb = R(ICON)
+
+@handler(ROUTE_PREFIX, NAME, thumb=ICON, art=ART)
+def MainMenu():
+
+ # Create a dictionary of menu items
+ mainMenuItems = OrderedDict([
+ ('Browse All Videos', {'function':BrowseVideos}),
+ ('Categories', {'function':BrowseCategories}),
+ ('Channels', {'function':BrowseChannels}),
+ ('Porn Stars', {'function':BrowsePornStars}),
+ ('Playlists', {'function':BrowsePlaylists}),
+ ('Members', {'function':BrowseMembers}),
+ ('Search', {'function':SearchVideos, 'search':True, 'directoryObjectArgs':{'prompt':'Search for...','summary':'Enter Search Terms'}})
+ ])
+
+ oc = GenerateMenu(NAME, mainMenuItems)
+
+ oc.add(PrefsObject(
+ title="Preferences"
+ ))
+
+ return oc
\ No newline at end of file
diff --git a/Contents/DefaultPrefs.json b/Contents/DefaultPrefs.json
new file mode 100644
index 0000000..ba9e55a
--- /dev/null
+++ b/Contents/DefaultPrefs.json
@@ -0,0 +1,82 @@
+[
+ {
+ "id": "channelBackgroundArt",
+ "label": "Background Art",
+ "type": "enum",
+ "values": ["default.jpg", "original.png", "alternate-1.jpg", "alternate-2.jpg", "stealth.png"],
+ "default": "default.jpg"
+ },
+ {
+ "id": "channelIconArt",
+ "label": "Icon Art",
+ "type": "enum",
+ "values": ["default.png", "stealth.png"],
+ "default": "default.png"
+ },
+ {
+ "id": "videoMenuShowThumbnails",
+ "label": "Video Menu - Show Thumbnails",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "videoMenuShowUploader",
+ "label": "Video Menu - Show Uploader",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "videoMenuShowPornStars",
+ "label": "Video Menu - Show Porn Stars",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "videoMenuShowRelatedVideos",
+ "label": "Video Menu - Show Related Videos",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "videoMenuShowPlaylists",
+ "label": "Video Menu - Show Playlists",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "videoMenuShowAction",
+ "label": "Video Menu - Show Action",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "memberMenuAccurateVideos",
+ "label": "Member Menu - Accurate Videos",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "memberMenuAccuratePlaylists",
+ "label": "Member Menu - Accurate Playlists",
+ "type": "bool",
+ "default": "true",
+ },
+ {
+ "id": "memberMenuAccurateSubscribers",
+ "label": "Member Menu - Accurate Subscribers",
+ "type": "bool",
+ "default": "false",
+ },
+ {
+ "id": "memberMenuAccurateMemberSubscriptions",
+ "label": "Member Menu - Accurate Member Subscriptions",
+ "type": "bool",
+ "default": "false",
+ },
+ {
+ "id": "memberMenuAccurateFriends",
+ "label": "Member Menu - Accurate Friends",
+ "type": "bool",
+ "default": "true",
+ }
+]
\ No newline at end of file
diff --git a/Contents/Info.plist b/Contents/Info.plist
index ab99edd..6bb2e7e 100644
--- a/Contents/Info.plist
+++ b/Contents/Info.plist
@@ -2,25 +2,17 @@
- CFBundleDevelopmentRegion
- English
- CFBundleExecutable
-
CFBundleIdentifier
com.plexapp.plugins.pornhub
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundlePackageType
- AAPL
- CFBundleSignature
- hook
- CFBundleVersion
- 1.0
PlexFrameworkVersion
- 1
+ 2
+ PlexClientPlatforms
+ *
PlexPluginDebug
- 1
- PlexPluginMode
- AlwaysOn
+
+
+
+ 1
+
diff --git a/Contents/Resources/art-alternate-1.jpg b/Contents/Resources/art-alternate-1.jpg
new file mode 100644
index 0000000..b7a6518
Binary files /dev/null and b/Contents/Resources/art-alternate-1.jpg differ
diff --git a/Contents/Resources/art-alternate-2.jpg b/Contents/Resources/art-alternate-2.jpg
new file mode 100644
index 0000000..c58033e
Binary files /dev/null and b/Contents/Resources/art-alternate-2.jpg differ
diff --git a/Contents/Resources/art-default.jpg b/Contents/Resources/art-default.jpg
new file mode 100644
index 0000000..ada3818
Binary files /dev/null and b/Contents/Resources/art-default.jpg differ
diff --git a/Contents/Resources/art-default.png b/Contents/Resources/art-original.png
similarity index 100%
rename from Contents/Resources/art-default.png
rename to Contents/Resources/art-original.png
diff --git a/Contents/Resources/icon-next.png b/Contents/Resources/icon-next.png
deleted file mode 100644
index 10d9414..0000000
Binary files a/Contents/Resources/icon-next.png and /dev/null differ
diff --git a/Contents/Services/ServiceInfo.plist b/Contents/Services/ServiceInfo.plist
new file mode 100644
index 0000000..c6643ef
--- /dev/null
+++ b/Contents/Services/ServiceInfo.plist
@@ -0,0 +1,16 @@
+
+
+
+
+ URL
+
+ pornhub
+
+ URLPatterns
+
+ ^http://([^.]+.)?pornhub\.com/.+
+
+
+
+
+
diff --git a/Contents/Services/Shared Code/PHCategories.pys b/Contents/Services/Shared Code/PHCategories.pys
new file mode 100644
index 0000000..198984b
--- /dev/null
+++ b/Contents/Services/Shared Code/PHCategories.pys
@@ -0,0 +1,21 @@
+def GetCategories(url):
+ # Create an empty list to hold the categories
+ categories = [];
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of catgegories
+ categoryElements = html.xpath("//div[@id='categoriesStraightImages']/ul[contains(@class, 'categories-list')]/li/div")
+
+ # Loop through all categories
+ for categoryElement in categoryElements:
+
+ # Use xPath to extract category details, and all the category to the list
+ categories.append({
+ 'title': categoryElement.xpath("./h5/a/strong/text()")[0],
+ 'url': categoryElement.xpath("./h5/a/@href")[0],
+ 'thumbnail': categoryElement.xpath("./a/img/@data-thumb_url")[0]
+ });
+
+ return categories
\ No newline at end of file
diff --git a/Contents/Services/Shared Code/PHChannels.pys b/Contents/Services/Shared Code/PHChannels.pys
new file mode 100644
index 0000000..977df7f
--- /dev/null
+++ b/Contents/Services/Shared Code/PHChannels.pys
@@ -0,0 +1,33 @@
+PH_CHANNEL_HOVER_URL = 'http://pornhub.com' + '/channel/hover?id=%s'
+
+def GetChannels(url):
+ # Create an empty list to hold the channels
+ channels = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of channels
+ channelElements = html.xpath("//div[contains(@class, 'listChannelsWrapper')]/ul/li/div")
+
+ # Loop through all channels
+ for channelElement in channelElements:
+
+ # Use xPath to extract the channel details, and add the channel to the list
+ channels.append({
+ 'title': channelElement.xpath("./div[contains(@class, 'description')]/div[contains(@class, 'descriptionContainer')]/ul/li/a[contains(@class, 'usernameLink')]/text()")[0],
+ 'url': channelElement.xpath("./div[contains(@class, 'description')]/div[contains(@class, 'descriptionContainer')]/ul/li/a[contains(@class, 'usernameLink')]/@href")[0],
+ 'thumbnail': channelElement.xpath("./div[contains(@class,'description')]/div[contains(@class, 'avatar')]/a/img/@src")[0]
+ })
+
+ return channels
+
+def GetChannelHoverMetaData(channelID):
+ # Fetch the porn star hover HTML
+ channelHoverHTML = HTML.ElementFromURL(PH_CHANNEL_HOVER_URL % channelID)
+
+ return {
+ 'name': channelHoverHTML.xpath("//div[contains(@class, 'avatarUserInfo')]/a[contains(@class,'username')]/text()")[0],
+ 'url': channelHoverHTML.xpath("//div[contains(@class, 'avatarUserInfo')]/a[contains(@class,'username')]/@href")[0],
+ 'thumbnail': channelHoverHTML.xpath("//div[contains(@class, 'avatarIcon')]/a/img/@src")[0]
+ }
\ No newline at end of file
diff --git a/Contents/Services/Shared Code/PHCommon.pys b/Contents/Services/Shared Code/PHCommon.pys
new file mode 100644
index 0000000..fde24ea
--- /dev/null
+++ b/Contents/Services/Shared Code/PHCommon.pys
@@ -0,0 +1,187 @@
+import json
+import time
+import urllib
+import urlparse
+from collections import OrderedDict
+
+PH_VIDEO_METADATA_JSON_REGEX = "var flashvars_\d+ = ({[\S\s]+?});"
+
+def GetVideoMetaDataJSON(htmlElement = None, url = None):
+ # Get the HTML of the site
+ if (htmlElement is None):
+ htmlElement = HTML.ElementFromURL(url)
+
+ htmlString = HTML.StringFromElement(htmlElement)
+
+ # Search for the video metadata JSON string
+ videoMetaDataString = Regex(PH_VIDEO_METADATA_JSON_REGEX).search(htmlString)
+
+ if (videoMetaDataString):
+ # If found, convert the JSON string to an object
+ return json.loads(videoMetaDataString.group(1))
+ else:
+ return None
+
+def GetVideos(url):
+ # Create an empty list to hold the categories
+ videos = []
+
+ # Get the HTML of the site
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of divs that contain videos
+ videoElements = html.xpath("//li[contains(@class,'videoblock')]")
+
+ # This piece of code is ridiculous. From the best I can gether, the poorly formed HTML on PornHub makes xPath choke at 123 videos. So I rounded it down to 120 and limited the videos to that. This should only affect playlists, but it is a really ridiculous problem
+ if (len(videoElements) >= 120):
+ videoElements = videoElements[0:120]
+
+ # Loop through the videos in the page
+ for videoElement in videoElements:
+ try:
+ # Use xPath to extract video details
+ video = {
+ 'title': videoElement.xpath(".//span[@class='title']/a/text()")[0],
+ 'url': videoElement.xpath(".//a/@href")[0],
+ 'thumbnail': videoElement.xpath(".//img/@data-mediumthumb")[0]
+ }
+
+ # Get the duration of the video
+ durationString = videoElement.xpath(".//var[@class='duration']/text()")[0]
+
+ # Split it into a list separated by colon
+ durationArray = durationString.split(":")
+
+ if (len(durationArray) == 2):
+ # Dealing with MM:SS
+ minutes = int(durationArray[0])
+ seconds = int(durationArray[1])
+
+ video["duration"] = (minutes*60 + seconds) * 1000
+
+ elif (len(durationArray) == 3):
+ # Dealing with HH:MM:SS... PornHub doesn't do this, but I'll keep it as a backup anyways
+ hours = int(durationArray[0])
+ minutes = int(durationArray[1])
+ seconds = int(durationArray[2])
+
+ video["duration"] = (hours*3600 + minutes * 60 + seconds) * 1000
+ else:
+ # Set a default duration of 0
+ video["duration"] = 0
+
+ videos.append(video)
+ except:
+ Log("Error encountered with one of the videos... skipping.")
+ pass
+ return videos
+
+def GetVideoThumbnailURLs(url):
+ # Create an empty list to hold the thumbnail URLs
+ thumbnailURLs = []
+
+ # Get the video meta data
+ videoMetaData = SharedCodeService.PHCommon.GetVideoMetaDataJSON(url=url)
+
+ if (videoMetaData["thumbs"] and videoMetaData["thumbs"]["urlPattern"] != False):
+
+ videoThumbnailsCount = Regex("S{(\d+)}.jpg$").search(videoMetaData["thumbs"]["urlPattern"])
+
+ if (videoThumbnailsCount):
+ videoThumbnailsCountString = videoThumbnailsCount.group(1)
+
+ for i in range(int(videoThumbnailsCountString) + 1):
+ thumbnailURLs.append(videoMetaData["thumbs"]["urlPattern"].replace("S{" + videoThumbnailsCountString + "}.jpg", "S" + str(i) + ".jpg"))
+
+ return thumbnailURLs
+
+def GetRelatedVideos(url):
+ # Create an empty list to hold the relatedVideos
+ relatedVideos = []
+
+ # Get the HTML of the site
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract the related videos
+ relatedVideoElements = html.xpath("//ul[@id='relatedVideosCenter' or @id='relateRecommendedItems']//li[contains(@class, 'videoblock')]/div[contains(@class, 'wrap')]")
+
+ # Loop through related videos
+ for relatedVideoElement in relatedVideoElements:
+ relatedVideos.append({
+ 'title': relatedVideoElement.xpath("./div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class,'title')]/a/text()")[0],
+ 'url': relatedVideoElement.xpath("./div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class,'title')]/a/@href")[0],
+ 'thumbnail': relatedVideoElement.xpath("./div[contains(@class, 'phimage')]/div[contains(@class, 'img')]/a/img/@data-mediumthumb")[0]
+ })
+
+ return relatedVideos
+
+def GetPlaylistsContainingVideo(url):
+ # Create an empty list to hold the playlists
+ playlists = []
+
+ # Get the HTML of the site
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract the playlists
+ playlistElements = html.xpath("//ul[contains(@class, 'playlist-listingSmall')]/li/div[contains(@class, 'wrap')]")
+
+ # Loop through playlists
+ for playlistElement in playlistElements:
+ playlists.append({
+ 'title': playlistElement.xpath("./div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class, 'title')]/a/text()")[0],
+ 'url': playlistElement.xpath("./div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class, 'title')]/a/@href")[0],
+ 'thumbnail': playlistElement.xpath("./div[contains(@class, 'linkWrapper')]/img/@data-mediumthumb")[0]
+ })
+
+ return playlists
+
+def GetVideoActions(url):
+ # Create an empty list to hold the actions
+ actions = []
+
+ # Get the video meta data
+ videoMetaData = SharedCodeService.PHCommon.GetVideoMetaDataJSON(url=url)
+
+ if (videoMetaData and videoMetaData["actionTags"]):
+ actionTags = videoMetaData["actionTags"].split(",")
+
+ for actionTag in actionTags:
+ actionSegments = actionTag.split(":")
+
+ actions.append({
+ 'title': actionSegments[0],
+ 'timestamp': time.strftime('%H:%M:%S', time.gmtime(int(actionSegments[1])))
+ })
+
+ return actions
+
+# I stole this function from http://stackoverflow.com/questions/2506379/add-params-to-given-url-in-python. It works.
+def AddURLParameters (url, params):
+
+ urlParts = list(urlparse.urlparse(url))
+
+ urlQuery = dict(urlparse.parse_qsl(urlParts[4]))
+ urlQuery.update(params)
+
+ # So... PornHub requires that it's query string parameters are set in the right order... for some reason. This piece of code handles that. It's retarded, but it has to be done
+ urlQueryOrder = ['c', 'channelSearch', 'search', 'username', 'o', 't', 'page']
+
+ urlQueryOrdered = OrderedDict()
+
+ for i in urlQueryOrder:
+ if i in urlQuery:
+ urlQueryOrdered[i] = urlQuery[i]
+
+ urlParts[4] = urllib.urlencode(urlQueryOrdered)
+
+ return urlparse.urlunparse(urlParts)
+
+# I stole this function (and everything I did for search basically) from the RedTube Plex Plugin, this file specifically https://github.com/flownex/RedTube.bundle/blob/master/Contents/Code/PCbfSearch.py
+def FormatStringForSearch(query, delimiter):
+ query = String.StripTags(str(query))
+ query = query.replace('%20',' ')
+ query = query.replace(' ',' ')
+ query = query.strip(' \t\n\r')
+ query = delimiter.join(query.split())
+
+ return query
diff --git a/Contents/Services/Shared Code/PHMembers.pys b/Contents/Services/Shared Code/PHMembers.pys
new file mode 100644
index 0000000..c02d11e
--- /dev/null
+++ b/Contents/Services/Shared Code/PHMembers.pys
@@ -0,0 +1,115 @@
+PH_MEMBER_HOVER_URL = 'http://pornhub.com' + '/user/hover?id=%s'
+PH_MEMBER_URL = '/users/%s'
+
+def GetMemberSortOrders(url):
+ # Create an empty list to hold the sort orders
+ sortOrders = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of sort orders
+ sortOrderElements = html.xpath("//div[contains(@class, 'members-page')]/div[contains(@class, 'sectionTitle')]")
+
+ # Loop through all sort orders
+ for sortOrderElement in sortOrderElements:
+
+ # Use xPath to extract sort order details, and add the sort order to the list
+ sortOrders.append({
+ 'title': sortOrderElement.xpath("./h2/text()")[0],
+ 'url': sortOrderElement.xpath("./div[contains(@class, 'filters')]/a/@href")[0]
+ })
+
+ return sortOrders
+
+def GetMembers(url):
+ # Create an empty list to hold the members
+ members = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of members
+ memberElements = html.xpath("//ul[contains(@class, 'userWidgetWrapperGrid')]/li/div[contains(@class, 'large-avatar')]/*[contains(@class, 'userLink')]/img")
+
+ # Loop through all members
+ for memberElement in memberElements:
+
+ # Use xPath to extract member details, and add the member to the list
+ members.append({
+ 'title': memberElement.xpath("./@title")[0],
+ 'url': PH_MEMBER_URL % memberElement.xpath("./@title")[0],
+ 'thumbnail': memberElement.xpath("./@src")[0]
+ })
+
+ return members
+
+def GetMemberChannels(url):
+ # Create an empty list to hold the channels
+ channels = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of channels
+ channelElements = html.xpath("//div[contains(@class, 'sectionWrapper')]/div[contains(@class, 'topheader')]")
+
+ for channelElement in channelElements:
+ # Use xPath to extract channel details, and add the channel to the list
+ channels.append({
+ 'title': channelElement.xpath("./div[contains(@class, 'floatLeft')]/div[contains(@class, 'title')]/a/text()")[0],
+ 'url': channelElement.xpath("./div[contains(@class, 'floatLeft')]/div[contains(@class, 'title')]/a/@href")[0] + "/videos",
+ 'thumbnail': channelElement.xpath("./div[contains(@class, 'avatarWrapper')]/a/img/@src")[0]
+ })
+
+ return channels
+
+def GetMemberSubscribedChannels(url):
+ # Create an empty list to hold the channels
+ channels = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of subscribed channels
+ channelElements = html.xpath("//div[contains(@class, 'channelSubWidgetContainer')]/ul/li[contains(@class, 'channelSubChannelWig')]")
+
+ for channelElement in channelElements:
+ # Use xPath to extract channel details, and add the channel to the list
+ channels.append({
+ 'title': channelElement.xpath("./div/div[contains(@class, 'wtitle')]/a/text()")[0],
+ 'url': channelElement.xpath("./div/div[contains(@class, 'wtitle')]/a/@href")[0] + "/videos",
+ 'thumbnail': channelElement.xpath("./div/div/a/img/@src")[0]
+ })
+
+ return channels
+
+def GetMemberSubscribedPornStars(url):
+ # Create an empty list to hold the porn stars
+ pornStars = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of subscribed porn stars
+ pornStarElements = html.xpath("//ul[contains(@class,'pornStarGrid')]/li/div[contains(@class,'user-flag')]/div[contains(@class,'avatarWrap')]/a")
+
+ for pornStarElement in pornStarElements:
+ # Use xPath to extract porn star details, and add the porn star to the list
+ pornStars.append({
+ 'title': pornStarElement.xpath("./img/@alt")[0],
+ 'url': pornStarElement.xpath("./@href")[0],
+ 'thumbnail': pornStarElement.xpath("./img/@src")[0]
+ })
+
+ return pornStars
+
+def GetMemberHoverMetaData(memberID):
+ # Fetch the porn star hover HTML
+ memberHoverHTML = HTML.ElementFromURL(PH_MEMBER_HOVER_URL % memberID)
+
+ return {
+ 'name': memberHoverHTML.xpath("//div[contains(@class, 'avatarUserInfo')]/a[contains(@class,'username')]/text()")[0],
+ 'url': memberHoverHTML.xpath("//div[contains(@class, 'avatarUserInfo')]/a[contains(@class,'username')]/@href")[0],
+ 'thumbnail': memberHoverHTML.xpath("//div[contains(@class, 'avatarIcon')]/a/img/@src")[0]
+ }
\ No newline at end of file
diff --git a/Contents/Services/Shared Code/PHPlaylists.pys b/Contents/Services/Shared Code/PHPlaylists.pys
new file mode 100644
index 0000000..9b7baed
--- /dev/null
+++ b/Contents/Services/Shared Code/PHPlaylists.pys
@@ -0,0 +1,31 @@
+def GetPlaylists(url):
+ # Create an empty list to hold the playlists
+ playlists = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of playlists
+ playlistElements = html.xpath("//ul[contains(@class, 'playlist-listing')]/li[contains(@id, 'playlist')]")
+
+ # Loop through all playlists
+ for playlistElement in playlistElements:
+
+ # Use xPath to extract playlist details
+ playlist = {
+ 'title': playlistElement.xpath("./div/div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class, 'title')]/a[contains(@class, 'title')]/text()")[0],
+ 'url': playlistElement.xpath("./div/div[contains(@class, 'thumbnail-info-wrapper')]/span[contains(@class, 'title')]/a[contains(@class, 'title')]/@href")[0]
+ }
+
+ # Make sure Playlist isn't empty
+ if (len(playlistElement.xpath(".//span[contains(@class,'playlist-videos')]/span[contains(@class,'number')]/span[text()='0']")) < 1):
+ playlist["isEmpty"] = False
+ playlist["thumbnail"] = playlistElement.xpath("./div/div[contains(@class, 'linkWrapper')]/img[contains(@class, 'largeThumb')]/@data-mediumthumb")[0]
+ else:
+ playlist["isEmpty"] = True
+ playlist["thumbnail"] = None
+
+ # Add the playlist to the list
+ playlists.append(playlist)
+
+ return playlists
\ No newline at end of file
diff --git a/Contents/Services/Shared Code/PHPornStars.pys b/Contents/Services/Shared Code/PHPornStars.pys
new file mode 100644
index 0000000..1440b40
--- /dev/null
+++ b/Contents/Services/Shared Code/PHPornStars.pys
@@ -0,0 +1,32 @@
+PH_PORNSTAR_HOVER_URL = 'http://pornhub.com' + '/pornstar/hover?id=%s'
+
+def GetPornStars(url):
+ # Create an empty list to hold the porn stars
+ pornStars = []
+
+ # Get the HTML of the page
+ html = HTML.ElementFromURL(url)
+
+ # Use xPath to extract a list of porn stars
+ pornStarElements = html.xpath("//ul[contains(@class, 'popular-pornstar')]/li")
+
+ # Loop through all channels
+ for pornStarElement in pornStarElements:
+
+ # Use xPath to extract porn star details, and add the porn star to the list
+ pornStars.append({
+ 'name': pornStarElement.xpath("./div/div[contains(@class, 'thumbnail-info-wrapper')]/a/text()")[0],
+ 'url': pornStarElement.xpath("./div/div[contains(@class, 'thumbnail-info-wrapper')]/a/@href")[0],
+ 'thumbnail': pornStarElement.xpath("./div/a/img/@data-thumb_url")[0]
+ })
+
+ return pornStars
+
+def GetPornStarHoverMetaData(pornStarID):
+ # Fetch the porn star hover HTML
+ pornStarHoverHTML = HTML.ElementFromURL(PH_PORNSTAR_HOVER_URL % pornStarID)
+
+ return {
+ 'name': pornStarHoverHTML.xpath("//div[@id='psInfoContainer']/div[@id='psBoxName']/text()")[0],
+ 'thumbnail': pornStarHoverHTML.xpath("//div[@id='psBoxPictureContainer']/img/@src")[0]
+ }
\ No newline at end of file
diff --git a/Contents/Services/URL/pornhub/ServiceCode.pys b/Contents/Services/URL/pornhub/ServiceCode.pys
new file mode 100644
index 0000000..6f24906
--- /dev/null
+++ b/Contents/Services/URL/pornhub/ServiceCode.pys
@@ -0,0 +1,86 @@
+import PHCommon
+
+PH_POTENTIAL_RESOLUTIONS = ["1080", "720", "480", "240", "180"]
+
+PH_VIDEO_URL_REGEX = '"quality":"%s","videoUrl":"([^\"]+)"'
+
+def NormalizeURL(url):
+
+ return url
+
+def MetadataObjectForURL(url):
+
+ # Get the HTML string from the given URL
+ html = HTML.ElementFromURL(url)
+
+ # Get the video meta data
+ videoMetaData = PHCommon.GetVideoMetaDataJSON(htmlElement=html)
+
+ if (videoMetaData):
+
+ return VideoClipObject(
+ title = videoMetaData["video_title"],
+ summary = videoMetaData["video_title"],
+ thumb = Resource.ContentsOfURLWithFallback(videoMetaData["image_url"], fallback='icon-default.jpg'),
+ content_rating = 'X',
+ duration = int(videoMetaData["video_duration"]) * 1000
+ )
+ else:
+ # Fall back to old xPath method
+ title = html.xpath('//title/text()')[0].strip()
+ thumbnail = html.xpath('//meta[@property="og:image"]/@content')[0].strip()
+ #tags = html.xpath('//div[@id="media-tags-container"]/h4/a/text()')
+
+ return VideoClipObject(
+ title = title,
+ summary = title,
+ thumb = Resource.ContentsOfURLWithFallback(thumbnail, fallback='icon-default.jpg'),
+ content_rating = 'X'
+ #tags = tags
+ )
+
+@deferred
+def MediaObjectsForURL(url):
+
+ # The list of MediaObjects to be returned
+ mediaObjects = []
+
+ # Get the HTML string from the given URL
+ html = HTML.ElementFromURL(url)
+ htmlString = HTML.StringFromElement(html)
+
+ # Get the video meta data
+ videoMetaData = PHCommon.GetVideoMetaDataJSON(htmlElement=html)
+
+ # Loop through all potential resolutions
+ for resolution in PH_POTENTIAL_RESOLUTIONS:
+ # Search for the video URL string
+ video = Regex(PH_VIDEO_URL_REGEX % resolution).search(htmlString)
+
+ # If video with the given resolution is found, add it to the list
+ if video:
+
+ mediaObject = MediaObject(
+ container = Container.MP4,
+ video_codec = VideoCodec.H264,
+ video_resolution = resolution,
+ audio_codec = AudioCodec.AAC,
+ audio_channels = 2,
+ optimized_for_streaming = True if Client.Product not in ['Plex Web'] else False,
+ parts = [
+ PartObject(
+ key = video.group(1).replace('\/', '/')
+ )
+ ]
+ )
+
+ # Check to see if extra metadata is available
+ if videoMetaData:
+ mediaObject.duration = int(videoMetaData["video_duration"]) * 1000
+
+ mediaObjects.append(mediaObject)
+
+
+ return mediaObjects
+
+ raise Ex.MediaNotAvailable
diff --git a/Contents/Site Configurations/pornhub.xml b/Contents/Site Configurations/pornhub.xml
deleted file mode 100644
index 2656ba2..0000000
--- a/Contents/Site Configurations/pornhub.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Contents/Strings/en.json b/Contents/Strings/en.json
new file mode 100644
index 0000000..49057af
--- /dev/null
+++ b/Contents/Strings/en.json
@@ -0,0 +1,11 @@
+{
+ "ChannelTitle": "PornHub",
+ "DefaultListVideosTitle": "Videos",
+ "DefaultBrowseVideosTitle": "Browse Videos",
+ "DefaultVideoMenuTitle": "Video Menu",
+ "DefaultBrowseCategoriesTitle": "Categories",
+ "DefaultBrowseChannelsTitle": "Channels",
+ "DefaultBrowsePornStarsTitle": "Porn Stars",
+ "DefaultBrowsePlaylistsTitle": "Playlists",
+ "DefaultBrowseMembersTitle": "Members"
+}
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..140f98f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# PornHub
+
+A Plex channel to view videos from the website PornHub (XXX).
+
+## Features
+
+* Browse videos with different sort options (most recent, most viewed, etc)
+* Categories
+* Channels
+* Porn Stars
+* Playlists
+* Members
+* Search
+
+## Installation
+
+This channel can be added through the Unsupported App Store v2
+([GitHub](https://github.com/ukdtom/UAS2Res)/[Plex Forum](https://forums.plex.tv/discussion/202282)).
+It can also be installed manually, however I would recommend using the UAS2.