Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@

- [ ] 馃憤 Yes, I have done the following tests:
- (please elaborate)
- [ ] 馃檯 No, because this PR is a draft and still needs work
- [ ] 馃敡 No, because this PR is a draft and still needs work.
- [ ] 馃檯 No, because this PR does not modify code.

## Screenshots

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,8 @@ To watch any video, simply click on your desired feed and locate the video you w


## Privacy and Security
- Your username, password and two-factor authentication code are never stored.
- Your password and two-factor authentication code are never stored.
- Your username is stored to make it faster to get playlists.
- However, your access token and refresh token (which gives the API access to your account) are stored in plaintext in the addon directory.
- These tokens will only last for at most 2 weeks, but treat them like your password and never share it nor your Kodi directory online.
- The plugin only stores data inside its respective directory inside the Kodi folder, except for cached results which are stored in a shared database in `temp/commoncache.db`.
Expand Down
203 changes: 186 additions & 17 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,9 +139,9 @@ def login(mode, token):
r = requests.post(url, data=payload, headers=headers)
status = r.status_code
credentials = r.json()
#error = dialog.ok('Error logging in', "Your account requires two factor authentication which is unsupported at this time. Sorry for the inconvenience.")

elif credentials["code"] == "invalid_token":
# If the token is invalid, don't call the logout function because it will also return an invalid token error
data = {}

# The other try block handles a file not found error, so we don't need to check here
Expand All @@ -158,7 +158,6 @@ def login(mode, token):
file.write(json.dumps(data, ensure_ascii=False, indent=4))

error = dialog.ok(__localize__(30008), __localize__(30009))

return
else:
error = dialog.ok(__localize__(30003), __localize__(30010))
Expand All @@ -178,7 +177,9 @@ def login(mode, token):
data = json.loads(content)

# Change / add the authenticated key to true
data["authenticated"] = True
data["authenticated"] = True
# Store the username for playlists
data["username"] = username

with xbmcvfs.File(DATA_PATH, 'w') as file:
# Save the file
Expand All @@ -190,8 +191,8 @@ def login(mode, token):
except (json.JSONDecodeError, FileNotFoundError):
# Open the file
with xbmcvfs.File(DATA_PATH, 'w') as file:
# Create the file and add authenticated = True
data = {"authenticated": True}
# Create the file and add authenticated = True, username = username (for playlists)
data = {"authenticated": True, "username": username}
file.write(json.dumps(data, ensure_ascii=False, indent=4))
# General catch statement
except Exception:
Expand Down Expand Up @@ -413,9 +414,104 @@ def search_menu(mode):

xbmcplugin.endOfDirectory(HANDLE)

# Get the playlist data.
# This is separated from the menu so it can be cached
def get_playlists(username):
try:
# Get the access token
access_token = get_token()

# If there was an error, also cancel this function
if not access_token:
return []

headers = {"Authorization": f"Bearer {access_token}"}

# Get all the playlists
playlistsReq = requests.get(f"{API}/accounts/{username}/video-playlists?playlistType=1", headers=headers)
playlists = playlistsReq.json()

# If there were no results
if "data" not in playlists or not playlists["data"]:
error = xbmcgui.Dialog().notification(__localize__(30011), __localize__(30032), xbmcgui.NOTIFICATION_ERROR)
# Raise so the menu function can stop
raise StopExecution("No results were found", data=[])

# Return the playlists
return playlists["data"]

except Exception as e:
error = dialog.notification(__localize__(30011), __localize__(30031).format(e), xbmcgui.NOTIFICATION_ERROR)
traceback.print_exc()
return []

# The playlists menu
def playlists_menu():
dialog = xbmcgui.Dialog()

listing = []

try:
DATA_PATH = USERDATA_PATH + "data.json"

# Get the username from the file
with xbmcvfs.File(DATA_PATH, 'r') as file:
# Read the data and load it to JSON
content = file.read()
data = json.loads(content)

# Get the username
username = data["username"]

playlists = cache.cacheFunction(get_playlists, username)

# The data probably exists if this occurs
for playlist in playlists:
name = playlist["displayName"]
playlistID = playlist["id"]

playlist = xbmcgui.ListItem(label=name)
playlistURL = get_url(action='listing', mode='playlist', playlistID=playlistID)
listing.append((playlistURL, playlist, True))

# Batch add once
xbmcplugin.addDirectoryItems(HANDLE, listing, len(listing))

xbmcplugin.endOfDirectory(HANDLE)

except StopExecution:
return
except KeyError:
# If the username isn't stored (e.g. after upgrading to this version) then login again
data = {}

# The other try block handles a file not found error, so we don't need to check here
with xbmcvfs.File(DATA_PATH, 'r') as file:
# Read the data and load it to JSON
content = file.read()
data = json.loads(content)

# Change the authenticated key to false
data["authenticated"] = False

with xbmcvfs.File(DATA_PATH, 'w') as file:
# Save the file
file.write(json.dumps(data, ensure_ascii=False, indent=4))

error = dialog.ok(__localize__(30008), __localize__(30009))
return

# If the file doesn't exist
except json.JSONDecodeError:
# This shouldn't be possible so pass
pass
except Exception as e:
error = dialog.notification(__localize__(30011), __localize__(30031).format(e), xbmcgui.NOTIFICATION_ERROR)
traceback.print_exc()
return False

# The selection menu of the addon
def menu():

# Check if the custom instance was set
if not CUSTOM_INSTANCE or CUSTOM_INSTANCE == "" or CUSTOM_INSTANCE.startswith("http"):
xbmcgui.Dialog().ok(__localize__(30020), __localize__(30021))
Expand Down Expand Up @@ -461,19 +557,29 @@ def menu():
if data["authenticated"] == True:
is_folder = True

# Subscriptions
subscriptions = xbmcgui.ListItem(label=__localize__(30027))
subscriptionsURL = get_url(action='listing', mode='subscriptions')

# Append subscriptions to listing variable
listing.append((subscriptionsURL, subscriptions, is_folder))

# Watch Later
watchLater = xbmcgui.ListItem(label=__localize__(30042))
watchLaterURL = get_url(action='listing', mode='watch_later')
listing.append((watchLaterURL, watchLater, is_folder))

# Playlists
playlists = xbmcgui.ListItem(label=__localize__(30043))
playlistsURL = get_url(action='playlists')
listing.append((playlistsURL, playlists, is_folder))

# Logout
logout = xbmcgui.ListItem(label=__localize__(30028))
logoutURL = get_url(action='logout')

# Append logout to listing variable
listing.append((logoutURL, logout, is_folder))

#xbmcplugin.addDirectoryItems(HANDLE, [(subscriptionsURL, subscriptions, is_folder), (logoutURL, logout, is_folder)])

else:
is_folder = True

Expand Down Expand Up @@ -561,8 +667,10 @@ def get_token():


# Must take instance_url to invalidate cache if the user changes their instance
def get_videos(instance_url, searchQuery, mode, page):

def get_videos(instance_url, searchQuery, mode, page, playlistID):
# TODO
# Make these requests, as well as the get_video function's requests, run in parallel with asyncio

start = page * 15
queryParams = f"?start={start}&count=15&hasHLSFiles=true"

Expand All @@ -573,7 +681,7 @@ def get_videos(instance_url, searchQuery, mode, page):

# If there was an error, also cancel this function
if not access_token:
return False
return []

headers = {"Authorization": f"Bearer {access_token}"}
request = requests.get(f"{API}/users/me/subscriptions/videos{queryParams}", headers=headers)
Expand All @@ -585,6 +693,27 @@ def get_videos(instance_url, searchQuery, mode, page):
request = requests.get(f"{API}/search/videos{queryParams}&search={searchQuery}")
elif mode == "global_search":
request = requests.get(f"https://sepiasearch.org/api/v1/search/videos{queryParams}&search={searchQuery}")
elif mode == "watch_later":
# Get the access key
access_token = get_token()

# If there was an error, also cancel this function
if not access_token:
return []

headers = {"Authorization": f"Bearer {access_token}"}

request = requests.get(f"{API}/video-playlists/{playlistID}/videos{queryParams}", headers=headers)
elif mode == "playlist":
# Get the access key
access_token = get_token()

# If there was an error, also cancel this function
if not access_token:
return []

headers = {"Authorization": f"Bearer {access_token}"}
request = requests.get(f"{API}/video-playlists/{playlistID}/videos{queryParams}", headers=headers)
else:
# Default request
request = requests.get(f"{API}/videos{queryParams}")
Expand Down Expand Up @@ -616,15 +745,43 @@ def generate_item_info(self, name, url, is_folder=True, thumbnail="",
}
}

def list_videos(mode, search, page):
def list_videos(mode, search, page, playlistID):
dialog = xbmcgui.Dialog()

# Cache results for 1 hour
# Must pass search here so it gets new data if the user searches for anything new
# Must pass CUSTOM_INSTANCE so it gets new data if the user changed their instance
try:
genre_info = cache.cacheFunction(get_videos, CUSTOM_INSTANCE, search, mode, page)
#genre_info = get_videos(CUSTOM_INSTANCE, search, mode, page)
# If it is a watch later request, get the playlist details here so it isn't cached
if mode == "watch_later":
# Get the access key
access_token = get_token()

# If there was an error, also cancel this function
if not access_token:
return

headers = {"Authorization": f"Bearer {access_token}"}

# Make a request to get the watch later playlist ID
playlistIDRequest = requests.get(f"{API}/users/me", headers=headers)

# Get the request in JSON
r = playlistIDRequest.json()

# If there were no results
if "specialPlaylists" not in r or not r["specialPlaylists"]:
error = xbmcgui.Dialog().notification(__localize__(30011), __localize__(30032), xbmcgui.NOTIFICATION_ERROR)
raise StopExecution("No results were found", data=[])

# Make sure the playlist is the watch later playlist
playlistID = 0
for playlist in r["specialPlaylists"]:
if playlist["name"] == "Watch later":
playlistID = playlist["id"]

genre_info = cache.cacheFunction(get_videos, CUSTOM_INSTANCE, search, mode, page, playlistID)
#genre_info = get_videos(CUSTOM_INSTANCE, search, mode, page, playlistID)
except StopExecution:
return

Expand All @@ -636,11 +793,15 @@ def list_videos(mode, search, page):
xbmcplugin.setPluginCategory(HANDLE, 'Videos')
xbmcplugin.setContent(HANDLE, 'movies')
videos = genre_info

# List of elements to add to the screen
listing = []

for video in videos:
# The watch later and playlist feeds add another key, 'video' that needs to be entered
if mode == "watch_later" or mode == "playlist":
video = video["video"]

xbmc.log(f'Title of video being processed: {video["name"]}')
list_item = xbmcgui.ListItem(label=video['name'])
info_tag = list_item.getVideoInfoTag()
Expand Down Expand Up @@ -869,7 +1030,13 @@ def router(paramstring):
except:
searchQuery = ""

list_videos(params['mode'], searchQuery, page)
# Handle no playlist ID being provided
try:
playlistID = params.get('playlistID', 2)
except:
playlistID = 0

list_videos(params['mode'], searchQuery, page, playlistID)
elif params['action'] == 'play':
play_video(params['video'])
elif params['action'] == 'login':
Expand All @@ -882,6 +1049,8 @@ def router(paramstring):
new_search(params['mode'])
elif params['action'] == 'delete_search':
delete_search(params['mode'], params['search'])
elif params['action'] == 'playlists':
playlists_menu()
else:
raise ValueError(f'Invalid paramstring: {paramstring}!')

Expand Down
9 changes: 9 additions & 0 deletions resources/language/resource.language.en_gb/strings.po
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ msgctxt "#30041"
msgid "Delete search"
msgstr ""

# Menu
msgctxt "#30042"
msgid "Watch Later"
msgstr ""

msgctxt "#30043"
msgid "Playlists"
msgstr ""

#Settings
#empty strings from id 30001 to 30099

Expand Down
Loading